Merge branch 'prlanzarin-2x-transcode-screenshare-merge' into bbb-2x-mconf

- I think there is still some work on messages between akka-apps and the video-transcoder.
  Merging so that we can do builds while Paulo works on the messages.
This commit is contained in:
Richard Alam 2017-07-17 19:22:03 -07:00
commit e11d69624f
131 changed files with 6730 additions and 183 deletions

View File

@ -191,7 +191,6 @@ class BigBlueButtonActor(
context.stop(m.actorRef)
}
}
}
}

View File

@ -15,6 +15,7 @@ import scala.concurrent.duration._
import org.bigbluebutton.core.pubsub.senders.UsersMessageToJsonConverter
import org.bigbluebutton.common.messages.UserEjectedFromMeetingMessage
import org.bigbluebutton.common.converters.ToJsonEncoder
import org.bigbluebutton.common.messages.StopMeetingTranscodersMessage
import scala.collection.JavaConverters
object MessageSenderActor {
@ -279,6 +280,11 @@ class MessageSenderActor(val service: MessageSender)
service.send(MessagingConstants.FROM_USERS_CHANNEL, json)
}
private def handleStopMeetingTranscoders(msg: StopMeetingTranscoders) {
val smt = new StopMeetingTranscodersMessage(msg.meetingID)
service.send(MessagingConstants.TO_BBB_TRANSCODE_SYSTEM_CHAN, smt.toJson())
}
private def handleGetGuestPolicyReply(msg: GetGuestPolicyReply) {
val json = UsersMessageToJsonConverter.getGuestPolicyReplyToJson(msg)
service.send(MessagingConstants.FROM_USERS_CHANNEL, json)

View File

@ -63,5 +63,8 @@ case class GetGuestPolicyReply(meetingID: String, recorded: Boolean, requesterID
case class GuestPolicyChanged(meetingID: String, recorded: Boolean, policy: String) extends IOutMessage
case class GuestAccessDenied(meetingID: String, recorded: Boolean, userId: String) extends IOutMessage
//Transcode
case class StopMeetingTranscoders(meetingID: String) extends IOutMessage
// Value Objects
case class MeetingVO(id: String, recorded: Boolean)

View File

@ -19,7 +19,7 @@ object AppsRedisSubscriberActor extends SystemConfiguration {
val TO_AKKA_APPS = "bbb:to-akka-apps"
val channels = Seq(toAkkaAppsRedisChannel, fromVoiceConfRedisChannel)
val patterns = Seq("bigbluebutton:to-bbb-apps:*", "bigbluebutton:from-voice-conf:*")
val patterns = Seq("bigbluebutton:to-bbb-apps:*", "bigbluebutton:from-voice-conf:*", "bigbluebutton:from-bbb-transcode:*")
def props(msgReceiver: RedisMessageReceiver, jsonMsgBus: IncomingJsonMessageBus): Props =
Props(classOf[AppsRedisSubscriberActor], msgReceiver, jsonMsgBus,

49
akka-bbb-transcode/.gitignore vendored Normal file
View File

@ -0,0 +1,49 @@
.metadata
.project
.classpath
.settings
.history
.worksheet
gen
**/*.swp
**/*~.nib
**/build/
**/*.pbxuser
**/*.perspective
**/*.perspectivev3
*.xcworkspace
*.xcuserdatad
*.iml
project/*.ipr
project/*.iml
project/*.iws
project/out
project/*/target
project/target
project/*/bin
project/*/build
project/*.iml
project/*/*.iml
project/.idea
project/.idea/*
.idea/
.DS_Store
project/.DS_Store
project/*/.DS_Store
tm.out
tmlog*.log
*.tm*.epoch
out/
provisioning/.vagrant
provisioning/*/.vagrant
provisioning/*/*.known
/sbt/akka-patterns-store/
/daemon/src/build/
*.lock
logs/
tmp/
build/
akka-patterns-store/
lib_managed/
.cache
bin/

View File

@ -0,0 +1,12 @@
# akka-bbb-transcode
This app implements a simple mechanism for media transcoding, acting as a relay between endpoints. This uses [FFmpeg](http://ffmpeg.org/) for transcoding between media formats and exchanging protocols.
building and running
---
```bash
cd akka-bbb-transcode/
sbt clean
sbt run
```

97
akka-bbb-transcode/build.sbt Executable file
View File

@ -0,0 +1,97 @@
enablePlugins(JavaServerAppPackaging)
name := "bbb-transcode-akka"
organization := "org.bigbluebutton"
version := "0.0.1"
scalaVersion := "2.11.6"
scalacOptions ++= Seq(
"-unchecked",
"-deprecation",
"-Xlint",
"-Ywarn-dead-code",
"-language:_",
"-target:jvm-1.7",
"-encoding", "UTF-8"
)
resolvers ++= Seq(
"spray repo" at "http://repo.spray.io/",
"rediscala" at "http://dl.bintray.com/etaty/maven",
"blindside-repos" at "http://blindside.googlecode.com/svn/repository/"
)
publishTo := Some(Resolver.file("file", new File(Path.userHome.absolutePath+"/dev/repo/maven-repo/releases" )) )
// We want to have our jar files in lib_managed dir.
// This way we'll have the right path when we import
// into eclipse.
retrieveManaged := true
testOptions in Test += Tests.Argument(TestFrameworks.Specs2, "html", "console", "junitxml")
testOptions in Test += Tests.Argument(TestFrameworks.ScalaTest, "-h", "target/scalatest-reports")
libraryDependencies ++= {
val akkaVersion = "2.3.11"
Seq(
"com.typesafe.akka" %% "akka-actor" % akkaVersion,
"com.typesafe.akka" %% "akka-testkit" % akkaVersion % "test",
"com.typesafe.akka" %% "akka-slf4j" % akkaVersion,
"ch.qos.logback" % "logback-classic" % "1.0.3",
"org.pegdown" % "pegdown" % "1.4.0",
"junit" % "junit" % "4.11",
"com.etaty.rediscala" %% "rediscala" % "1.4.0",
"commons-codec" % "commons-codec" % "1.8",
"joda-time" % "joda-time" % "2.3",
"com.google.code.gson" % "gson" % "1.7.1",
"redis.clients" % "jedis" % "2.1.0",
"org.apache.commons" % "commons-lang3" % "3.2",
"org.bigbluebutton" % "bbb-common-message_2.12" % "0.0.19-SNAPSHOT"
)}
seq(Revolver.settings: _*)
scalariformSettings
//-----------
// Packaging
//
// Reference:
// https://github.com/muuki88/sbt-native-packager-examples/tree/master/akka-server-app
// http://www.scala-sbt.org/sbt-native-packager/index.html
//-----------
mainClass := Some("org.bigbluebutton.Boot")
maintainer in Linux := "Mario Gasparoni <mariogasparoni@gmail.com>"
packageSummary in Linux := "BigBlueButton Transcoder"
packageDescription := """BigBlueButton FFmpeg transcoder."""
val user = "bigbluebutton"
val group = "bigbluebutton"
// user which will execute the application
daemonUser in Linux := user
// group which will execute the application
daemonGroup in Linux := group
mappings in Universal <+= (packageBin in Compile, sourceDirectory ) map { (_, src) =>
// Move the application.conf so the user can override settings here
val appConf = src / "main" / "resources" / "application.conf"
appConf -> "conf/application.conf"
}
mappings in Universal <+= (packageBin in Compile, sourceDirectory ) map { (_, src) =>
// Move logback.xml so the user can override settings here
val logConf = src / "main" / "resources" / "logback.xml"
logConf -> "conf/logback.xml"
}
debianPackageDependencies in Debian ++= Seq("java7-runtime-headless", "bash")

View File

View File

@ -0,0 +1 @@
sbt.version=0.13.8

View File

@ -0,0 +1,7 @@
addSbtPlugin("io.spray" % "sbt-revolver" % "0.7.2")
addSbtPlugin("com.typesafe.sbt" % "sbt-scalariform" % "1.3.0")
addSbtPlugin("com.typesafe.sbteclipse" % "sbteclipse-plugin" % "2.2.0")
addSbtPlugin("com.typesafe.sbt" % "sbt-native-packager" % "1.0.0")

View File

@ -0,0 +1,20 @@
package org.bigbluebutton.transcode.api;
public class DestroyVideoTranscoderReply extends InternalMessage {
private final String meetingId;
private final String transcoderId;
public DestroyVideoTranscoderReply(String meetingId, String transcoderId) {
this.meetingId = meetingId;
this.transcoderId = transcoderId;
}
public String getMeetingId() {
return meetingId;
}
public String getTranscoderId() {
return transcoderId;
}
}

View File

@ -0,0 +1,6 @@
package org.bigbluebutton.transcode.api;
public class DestroyVideoTranscoderRequest extends InternalMessage {
public DestroyVideoTranscoderRequest() {}
}

View File

@ -0,0 +1,3 @@
package org.bigbluebutton.transcode.api;
public class InternalMessage {}

View File

@ -0,0 +1,26 @@
package org.bigbluebutton.transcode.api;
public class RestartVideoTranscoderReply extends InternalMessage {
private final String meetingId;
private final String transcoderId;
private final String output;
public RestartVideoTranscoderReply(String meetingId, String transcoderId, String output) {
this.meetingId = meetingId;
this.transcoderId = transcoderId;
this.output = output;
}
public String getMeetingId() {
return meetingId;
}
public String getTranscoderId() {
return transcoderId;
}
public String getOutput() {
return output;
}
}

View File

@ -0,0 +1,5 @@
package org.bigbluebutton.transcode.api;
public class RestartVideoTranscoderRequest extends InternalMessage {
public RestartVideoTranscoderRequest() {}
}

View File

@ -0,0 +1,27 @@
package org.bigbluebutton.transcode.api;
import java.util.Map;
public class StartVideoProbingReply extends InternalMessage {
private final String meetingId;
private final String transcoderId;
private final Map<String,String> probingData;
public StartVideoProbingReply (String meetingId, String transcoderId, Map<String,String> probingData) {
this.meetingId = meetingId;
this.transcoderId = transcoderId;
this.probingData = probingData;
}
public String getMeetingId() {
return meetingId;
}
public String getTranscoderId() {
return transcoderId;
}
public Map<String,String> getProbingData(){
return probingData;
}
}

View File

@ -0,0 +1,5 @@
package org.bigbluebutton.transcode.api;
public class StartVideoProbingRequest extends InternalMessage {
public StartVideoProbingRequest() {}
}

View File

@ -0,0 +1,26 @@
package org.bigbluebutton.transcode.api;
public class StartVideoTranscoderReply extends InternalMessage {
private final String meetingId;
private final String transcoderId;
private final String output;
public StartVideoTranscoderReply(String meetingId, String transcoderId, String output) {
this.meetingId = meetingId;
this.transcoderId = transcoderId;
this.output = output;
}
public String getMeetingId() {
return meetingId;
}
public String getTranscoderId() {
return transcoderId;
}
public String getOutput() {
return output;
}
}

View File

@ -0,0 +1,5 @@
package org.bigbluebutton.transcode.api;
public class StartVideoTranscoderRequest extends InternalMessage {
public StartVideoTranscoderRequest() {}
}

View File

@ -0,0 +1,20 @@
package org.bigbluebutton.transcode.api;
public class StopVideoTranscoderReply extends InternalMessage {
private final String meetingId;
private final String transcoderId;
public StopVideoTranscoderReply(String meetingId, String transcoderId) {
this.meetingId = meetingId;
this.transcoderId = transcoderId;
}
public String getMeetingId() {
return meetingId;
}
public String getTranscoderId() {
return transcoderId;
}
}

View File

@ -0,0 +1,5 @@
package org.bigbluebutton.transcode.api;
public class StopVideoTranscoderRequest extends InternalMessage {
public StopVideoTranscoderRequest() {}
}

View File

@ -0,0 +1,19 @@
package org.bigbluebutton.transcode.api;
public class TranscodingFinishedSuccessfully extends InternalMessage {
private final String meetingId;
private final String transcoderId;
public TranscodingFinishedSuccessfully (String meetingId, String transcoderId) {
this.meetingId = meetingId;
this.transcoderId = transcoderId;
}
public String getMeetingId() {
return meetingId;
}
public String getTranscoderId() {
return transcoderId;
}
}

View File

@ -0,0 +1,19 @@
package org.bigbluebutton.transcode.api;
public class TranscodingFinishedUnsuccessfully extends InternalMessage {
private final String meetingId;
private final String transcoderId;
public TranscodingFinishedUnsuccessfully (String meetingId, String transcoderId) {
this.meetingId = meetingId;
this.transcoderId = transcoderId;
}
public String getMeetingId() {
return meetingId;
}
public String getTranscoderId() {
return transcoderId;
}
}

View File

@ -0,0 +1,26 @@
package org.bigbluebutton.transcode.api;
import java.util.Map;
public class UpdateVideoTranscoderReply extends InternalMessage {
private final String meetingId;
private final String transcoderId;
private final String output;
public UpdateVideoTranscoderReply(String meetingId, String transcoderId, String output) {
this.meetingId = meetingId;
this.transcoderId = transcoderId;
this.output = output;
}
public String getMeetingId() {
return meetingId;
}
public String getTranscoderId() {
return transcoderId;
}
public String getOutput() {
return output;
}
}

View File

@ -0,0 +1,15 @@
package org.bigbluebutton.transcode.api;
import java.util.Map;
public class UpdateVideoTranscoderRequest extends InternalMessage {
private final Map<String,String> params;
public UpdateVideoTranscoderRequest(Map<String,String> params) {
this.params = params;
}
public Map<String,String> getParams() {
return params;
}
}

View File

@ -0,0 +1,933 @@
package org.bigbluebutton.transcode.core;
import java.io.BufferedReader;
import java.io.IOException;
import java.io.StringReader;
import java.util.HashMap;
import java.util.Map;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import org.bigbluebutton.transcode.core.ffprobe.FFProbeCommand;
import org.bigbluebutton.transcode.core.ffmpeg.FFmpegCommand;
import org.bigbluebutton.transcode.core.ffmpeg.FFmpegConstants;
import org.bigbluebutton.transcode.core.ffmpeg.FFmpegUtils;
import org.bigbluebutton.transcode.core.processmonitor.ProcessMonitor;
import org.bigbluebutton.transcode.core.processmonitor.ProcessMonitorObserver;
import org.bigbluebutton.common.messages.Constants;
import akka.actor.UntypedActor;
import akka.actor.ActorRef;
import akka.actor.Props;
import akka.japi.Creator;
import org.bigbluebutton.transcode.api.InternalMessage;
import org.bigbluebutton.transcode.api.DestroyVideoTranscoderRequest;
import org.bigbluebutton.transcode.api.DestroyVideoTranscoderReply;
import org.bigbluebutton.transcode.api.RestartVideoTranscoderRequest;
import org.bigbluebutton.transcode.api.RestartVideoTranscoderReply;
import org.bigbluebutton.transcode.api.StartVideoProbingRequest;
import org.bigbluebutton.transcode.api.StartVideoProbingReply;
import org.bigbluebutton.transcode.api.StartVideoTranscoderRequest;
import org.bigbluebutton.transcode.api.StartVideoTranscoderReply;
import org.bigbluebutton.transcode.api.StopVideoTranscoderRequest;
import org.bigbluebutton.transcode.api.StopVideoTranscoderReply;
import org.bigbluebutton.transcode.api.UpdateVideoTranscoderRequest;
import org.bigbluebutton.transcode.api.UpdateVideoTranscoderReply;
import org.bigbluebutton.transcode.api.TranscodingFinishedSuccessfully;
import org.bigbluebutton.transcode.api.TranscodingFinishedUnsuccessfully;
public class VideoTranscoder extends UntypedActor implements ProcessMonitorObserver {
public static enum Type{
TRANSCODE_RTP_TO_RTMP,
TRANSCODE_RTMP_TO_RTP,
TRANSCODE_FILE_TO_RTP,
TRANSCODE_FILE_TO_RTMP,
TRANSCODE_H264_TO_H263,
TRANSCODE_ROTATE_RIGHT,
TRANSCODE_ROTATE_LEFT,
TRANSCODE_ROTATE_UPSIDE_DOWN,
PROBE_RTMP
};
public static enum Status{RUNNING, STOPPED, UPDATING}
public static final String VIDEO_CONF_LOGO_PATH = FFmpegUtils.videoconfLogoPath;
public static final String FFMPEG_PATH = FFmpegUtils.ffmpegPath;
public static final String FFPROBE_PATH = FFmpegUtils.ffprobePath;
//if ffmpeg restarts 5 times in less than 5 seconds, we will not restart it anymore
//this is to prevent a infinite loop of ffmpeg restartings
private static final int MAX_RESTARTINGS_NUMBER = 5;
private static final long MIN_RESTART_TIME = 5000; //5 seconds
private int currentFFmpegRestartNumber = 0;
private long lastFFmpegRestartTime = 0;
private ActorRef parentActor;
private Type type;
private Status status = Status.STOPPED;
private ProcessMonitor ffmpegProcessMonitor;
private ProcessMonitor ffprobeProcessMonitor;
private FFmpegCommand ffmpeg;
private String transcoderId;
private String username;
private String callername; //used to create rtp-> (any) SDP
private String videoStreamName;
private String input;
private String outputLive;
private String output; //output of transcoder
private String meetingId;
private String voiceBridge;
private String sourceIp;
private String destinationIp;
private String localVideoPort;
private String remoteVideoPort;
private String sdpPath;
private String sourceModule;
private VideoTranscoderObserver observer;
private String globalVideoWidth = "640";// get this from properties (Stored in FFmpegUtils)
private String globalVideoHeight = "480";// get this from properties
public static final String FFMPEG_NAME = "FFMPEG";
public static final String FFPROBE_NAME = "FFPROBE";
public static Props props(final ActorRef parentActor, final String meetingId, final String transcoderId, final Map<String,String> params) {
return Props.create(new Creator<VideoTranscoder>() {
private static final long serialVersionUID = 1L;
@Override
public VideoTranscoder create() throws Exception {
return new VideoTranscoder(parentActor, meetingId, transcoderId, params);
}
});
}
@Override
public void onReceive(Object msg) {
if (msg instanceof StartVideoTranscoderRequest) {
start();
} else if (msg instanceof StopVideoTranscoderRequest) {
stop();
} else if (msg instanceof UpdateVideoTranscoderRequest) {
UpdateVideoTranscoderRequest uvtr = (UpdateVideoTranscoderRequest) msg;
update(uvtr.getParams());
} else if (msg instanceof DestroyVideoTranscoderRequest) {
destroyTranscoder();
} else if (msg instanceof RestartVideoTranscoderRequest) {
restart();
} else if (msg instanceof StartVideoProbingRequest) {
probeVideoStream();
}
}
private void stopActor() {
if (context() != null)
context().stop(getSelf());
}
private void sendMessage(InternalMessage msg) {
if ((parentActor != null) && (msg != null))
parentActor.tell(msg,getSelf());
}
public VideoTranscoder(ActorRef parentActor, String meetingId, String transcoderId, Map<String,String> params){
this.parentActor = parentActor;
this.meetingId = meetingId;
this.transcoderId = transcoderId;
if (params != null)
switch (params.get(Constants.TRANSCODER_TYPE)){
case Constants.TRANSCODE_RTP_TO_RTMP:
this.type = Type.TRANSCODE_RTP_TO_RTMP;
this.sourceIp = params.get(Constants.LOCAL_IP_ADDRESS);
this.localVideoPort = params.get(Constants.LOCAL_VIDEO_PORT);
this.remoteVideoPort = params.get(Constants.REMOTE_VIDEO_PORT);
this.destinationIp = params.get(Constants.DESTINATION_IP_ADDRESS);
this.voiceBridge = params.get(Constants.VOICE_CONF);
this.callername = params.get(Constants.CALLERNAME);
break;
case Constants.TRANSCODE_RTMP_TO_RTP:
this.type = Type.TRANSCODE_RTMP_TO_RTP;
this.sourceIp = params.get(Constants.LOCAL_IP_ADDRESS);
this.localVideoPort = params.get(Constants.LOCAL_VIDEO_PORT);
this.remoteVideoPort = params.get(Constants.REMOTE_VIDEO_PORT);
this.destinationIp = params.get(Constants.DESTINATION_IP_ADDRESS);
this.voiceBridge = params.get(Constants.VOICE_CONF);
this.callername = params.get(Constants.CALLERNAME);
this.videoStreamName = params.get(Constants.INPUT);
break;
case Constants.TRANSCODE_FILE_TO_RTP:
this.type = Type.TRANSCODE_FILE_TO_RTP;
this.sourceIp = params.get(Constants.LOCAL_IP_ADDRESS);
this.localVideoPort = params.get(Constants.LOCAL_VIDEO_PORT);
this.remoteVideoPort = params.get(Constants.REMOTE_VIDEO_PORT);
this.destinationIp = params.get(Constants.DESTINATION_IP_ADDRESS);
this.voiceBridge = params.get(Constants.VOICE_CONF);
this.callername = params.get(Constants.CALLERNAME);
break;
case Constants.TRANSCODE_FILE_TO_RTMP:
this.type = Type.TRANSCODE_FILE_TO_RTMP;
this.sourceIp = params.get(Constants.LOCAL_IP_ADDRESS);
this.localVideoPort = params.get(Constants.LOCAL_VIDEO_PORT);
this.remoteVideoPort = params.get(Constants.REMOTE_VIDEO_PORT);
this.destinationIp = params.get(Constants.DESTINATION_IP_ADDRESS);
this.voiceBridge = params.get(Constants.VOICE_CONF);
this.callername = params.get(Constants.CALLERNAME);
break;
case Constants.TRANSCODE_H264_TO_H263:
this.type = Type.TRANSCODE_H264_TO_H263;
this.sourceModule = params.get(Constants.MODULE);
this.sourceIp = params.get(Constants.LOCAL_IP_ADDRESS);
this.destinationIp = params.get(Constants.DESTINATION_IP_ADDRESS);
this.videoStreamName = params.get(Constants.INPUT);
break;
case Constants.TRANSCODE_ROTATE_RIGHT:
this.type = Type.TRANSCODE_ROTATE_RIGHT;
this.sourceIp = params.get(Constants.LOCAL_IP_ADDRESS);
this.destinationIp = params.get(Constants.DESTINATION_IP_ADDRESS);
this.videoStreamName = params.get(Constants.INPUT);
break;
case Constants.TRANSCODE_ROTATE_LEFT:
this.type = Type.TRANSCODE_ROTATE_LEFT;
this.sourceIp = params.get(Constants.LOCAL_IP_ADDRESS);
this.destinationIp = params.get(Constants.DESTINATION_IP_ADDRESS);
this.videoStreamName = params.get(Constants.INPUT);
break;
case Constants.TRANSCODE_ROTATE_UPSIDE_DOWN:
this.type = Type.TRANSCODE_ROTATE_UPSIDE_DOWN;
this.sourceIp = params.get(Constants.LOCAL_IP_ADDRESS);
this.destinationIp = params.get(Constants.DESTINATION_IP_ADDRESS);
this.videoStreamName = params.get(Constants.INPUT);
break;
case Constants.PROBE_RTMP:
this.type = Type.PROBE_RTMP;
this.videoStreamName = params.get(Constants.INPUT);
break;
default:
break;
}
}
private synchronized void start() {
switch (status){
case RUNNING:
System.out.println(" > Transcoder already running, sending it's output");
break;
case UPDATING:
System.out.println(" > Transcoder is being updated, returning it's current output");
break;
default:
status = Status.RUNNING;
startTranscoder();
break;
}
sendMessage(new StartVideoTranscoderReply(meetingId, transcoderId, output));
}
private boolean startTranscoder(){
if ((ffmpegProcessMonitor != null) &&(ffmpeg != null)) {
return false;
}
String[] command;
if(!canFFmpegRun()) {
//log.debug("***TRANSCODER WILL NOT START: ffmpeg cannot run");
return false;
}
//log.debug("Starting Video Transcoder...");
switch(type){
case TRANSCODE_RTMP_TO_RTP:
if(!areRtmpToRtpParametersValid()) {
System.out.println(" > ***TRANSCODER WILL NOT START: Rtmp to Rtp Parameters are invalid");
return false;
}
input = "rtmp://" + sourceIp + "/video/" + meetingId + "/"
+ videoStreamName + " live=1"; //the full input is composed by the videoStreamName
outputLive = "rtp://" + destinationIp + ":" + remoteVideoPort + "?localport=" + localVideoPort;
output = "";
ffmpeg = new FFmpegCommand();
ffmpeg.setFFmpegPath(FFMPEG_PATH);
ffmpeg.setInput(input);
ffmpeg.addRtmpInputConnectionParameter(meetingId);
ffmpeg.addRtmpInputConnectionParameter("transcoder-"+transcoderId);
ffmpeg.setFrameRate(15);
ffmpeg.setBufSize(1024);
ffmpeg.setGop(1); //MCU compatibility
ffmpeg.setCodec("libopenh264");
ffmpeg.setMaxRate(1024);
ffmpeg.setSliceMode("dyn");
ffmpeg.setMaxNalSize("1024");
ffmpeg.setRtpFlags("h264_mode0"); //RTP's packetization mode 0
ffmpeg.setProfile("baseline");
ffmpeg.setFormat("rtp");
ffmpeg.setPayloadType(FFmpegConstants.CODEC_ID_H264);
ffmpeg.setLoglevel("verbose");
ffmpeg.setOutput(outputLive);
ffmpeg.setAnalyzeDuration("1000"); // 1ms
ffmpeg.setProbeSize("32"); // 1ms
System.out.println("Preparing FFmpeg process monitor");
command = ffmpeg.getFFmpegCommand(true);
break;
case TRANSCODE_RTP_TO_RTMP:
if(!areRtpToRtmpParametersValid()) {
System.out.println(" > ***TRANSCODER WILL NOT START: Rtp to Rtmp Parameters are invalid");
return false;
}
//Create SDP FILE
sdpPath = FFmpegUtils.createSDPVideoFile(callername, sourceIp, localVideoPort, FFmpegConstants.CODEC_NAME_H264, FFmpegConstants.CODEC_ID_H264, FFmpegConstants.SAMPLE_RATE_H264, voiceBridge);
input = sdpPath;
//Generate video stream name
videoStreamName = generateVideoStreamName(type);
// TODO make stream path dynamic by turning it into a param
outputLive = "rtmp://" + destinationIp + "/video-broadcast/" + meetingId + "/"
+ videoStreamName+" live=1";
output = videoStreamName;
ffmpeg = new FFmpegCommand();
ffmpeg.setFFmpegPath(FFMPEG_PATH);
ffmpeg.setInput(input);
ffmpeg.setLoglevel("quiet");
ffmpeg.setOutput(outputLive);
ffmpeg.addRtmpOutputConnectionParameter(meetingId);
ffmpeg.addRtmpOutputConnectionParameter("transcoder-"+transcoderId);
ffmpeg.setCodec("copy");
ffmpeg.setFormat("flv");
command = ffmpeg.getFFmpegCommand(true);
break;
case TRANSCODE_FILE_TO_RTP:
if(!areFileToRtpParametersValid()) {
System.out.println(" > ***TRANSCODER WILL NOT START: File to Rtp Parameters are invalid");
return false;
}
input = VIDEO_CONF_LOGO_PATH;
outputLive = "rtp://" + destinationIp + ":" + remoteVideoPort + "?localport=" + localVideoPort;
output = "";
username = callername;
ffmpeg = new FFmpegCommand();
ffmpeg.setFFmpegPath(FFMPEG_PATH);
ffmpeg.setIgnoreLoop(0);
ffmpeg.setInput(input);
ffmpeg.setInputLive(true);
ffmpeg.addCustomParameter("-s", globalVideoWidth+"x"+globalVideoHeight);
ffmpeg.setFrameRate(15);
ffmpeg.setPayloadType(FFmpegConstants.CODEC_ID_H264);
ffmpeg.setLoglevel("quiet");
if (FFmpegUtils.isUserVideoSubtitleEnabled())
ffmpeg.addCustomParameter("-vf","drawtext=fontfile=/usr/share/fonts/truetype/liberation/LiberationSans-Bold.ttf:text="+username+":x="+globalVideoWidth+"-tw-20:y="+globalVideoHeight+"-th-20:fontcolor=white@0.9:shadowcolor=black:shadowx=2:shadowy=2:fontsize=20");
ffmpeg.setGop(1);
ffmpeg.setCodec("libopenh264");
ffmpeg.setSliceMode("dyn");
ffmpeg.setMaxNalSize("1024");
ffmpeg.setRtpFlags("h264_mode0"); //RTP's packetization mode 0
ffmpeg.setProfile("baseline");
ffmpeg.setFormat("rtp");
ffmpeg.setOutput(outputLive);
command = ffmpeg.getFFmpegCommand(true);
break;
case TRANSCODE_FILE_TO_RTMP:
if(!areFileToRtmpParametersValid()) {
System.out.println("***TRANSCODER WILL NOT START: File to Rtmp Parameters are invalid");
return false;
}
videoStreamName = generateVideoStreamName(type);
input = VIDEO_CONF_LOGO_PATH;
outputLive = "rtmp://" + destinationIp + "/video/" + meetingId + "/"
+ videoStreamName+" live=1";
output = videoStreamName;
ffmpeg = new FFmpegCommand();
ffmpeg.setFFmpegPath(FFMPEG_PATH);
ffmpeg.setInput(input);
ffmpeg.setInputLive(true);
ffmpeg.setFrameSize("640x480");
ffmpeg.setIgnoreLoop(0);
ffmpeg.setFormat("flv");
ffmpeg.setLoglevel("verbose");
ffmpeg.addRtmpOutputConnectionParameter(meetingId);
ffmpeg.addRtmpOutputConnectionParameter("transcoder-"+transcoderId);
ffmpeg.setOutput(outputLive);
ffmpeg.setCodec("libopenh264");
ffmpeg.setProfile("baseline");
command = ffmpeg.getFFmpegCommand(true);
break;
case TRANSCODE_H264_TO_H263:
if(!areH264ToH263ParametersValid()) {
System.out.println(" > ***TRANSCODER WILL NOT START: H264 to H263 parameters are invalid");
return false;
}
switch(sourceModule) {
case FFmpegUtils.VIDEO_MODULE:
input = "rtmp://" + sourceIp + "/" + sourceModule + "/" + meetingId + "/" + videoStreamName + " live=1";
outputLive = "rtmp://" + destinationIp + "/" + sourceModule + "/" + meetingId + "/" + FFmpegUtils.H263PREFIX + "/" + videoStreamName;
output = videoStreamName;
break;
case FFmpegUtils.DESKSHARE_MODULE:
input = "rtmp://" + sourceIp + "/" + sourceModule + "/" + meetingId + " live=1";
outputLive = "rtmp://" + destinationIp + "/" + sourceModule + "/" + FFmpegUtils.H263PREFIX + "/" + meetingId;
output = meetingId;
break;
default:
System.out.println(" > ***TRANSCODER WILL NOT START: Unrecognized module: " + sourceModule);
}
ffmpeg = new FFmpegCommand();
ffmpeg.setFFmpegPath(FFMPEG_PATH);
ffmpeg.setInput(input);
ffmpeg.setCodec("flv1"); // Sorensen H263
ffmpeg.setFormat("flv");
ffmpeg.addRtmpOutputConnectionParameter(meetingId);
ffmpeg.addRtmpOutputConnectionParameter(transcoderId);
ffmpeg.setOutput(outputLive);
ffmpeg.setLoglevel("quiet");
ffmpeg.setAnalyzeDuration("10000"); // 10ms
command = ffmpeg.getFFmpegCommand(true);
break;
case TRANSCODE_ROTATE_RIGHT:
if(!areRotateParametersValid()) {
System.out.println(" > ***TRANSCODER WILL NOT START: Rotate parameters are invalid");
return false;
}
input = "rtmp://" + sourceIp + "/video/" + meetingId + "/" + FFmpegUtils.ROTATE_RIGHT + "/" + videoStreamName + " live=1";
outputLive = "rtmp://" + destinationIp + "/video/" + meetingId + "/" + videoStreamName;
output = videoStreamName;
ffmpeg = new FFmpegCommand();
ffmpeg.setFFmpegPath(FFMPEG_PATH);
ffmpeg.setInput(input);
ffmpeg.setFormat("flv");
ffmpeg.addRtmpOutputConnectionParameter(meetingId);
ffmpeg.addRtmpOutputConnectionParameter(transcoderId);
ffmpeg.setOutput(outputLive);
ffmpeg.setLoglevel("warning");
ffmpeg.setRotation(FFmpegUtils.ROTATE_RIGHT);
ffmpeg.setAnalyzeDuration("10000"); // 10ms
command = ffmpeg.getFFmpegCommand(true);
break;
case TRANSCODE_ROTATE_LEFT:
if(!areRotateParametersValid()) {
System.out.println(" > ***TRANSCODER WILL NOT START: Rotate parameters are invalid");
return false;
}
input = "rtmp://" + sourceIp + "/video/" + meetingId + "/" + FFmpegUtils.ROTATE_LEFT + "/" + videoStreamName + " live=1";
outputLive = "rtmp://" + destinationIp + "/video/" + meetingId + "/" + videoStreamName;
output = videoStreamName;
ffmpeg = new FFmpegCommand();
ffmpeg.setFFmpegPath(FFMPEG_PATH);
ffmpeg.setInput(input);
ffmpeg.setFormat("flv");
ffmpeg.addRtmpOutputConnectionParameter(meetingId);
ffmpeg.addRtmpOutputConnectionParameter(transcoderId);
ffmpeg.setOutput(outputLive);
ffmpeg.setLoglevel("warning");
ffmpeg.setRotation(FFmpegUtils.ROTATE_LEFT);
ffmpeg.setAnalyzeDuration("10000"); // 10ms
command = ffmpeg.getFFmpegCommand(true);
break;
case TRANSCODE_ROTATE_UPSIDE_DOWN:
if(!areRotateParametersValid()) {
System.out.println(" > ***TRANSCODER WILL NOT START: Rotate parameters are invalid");
return false;
}
input = "rtmp://" + sourceIp + "/video/" + meetingId + "/" + FFmpegUtils.ROTATE_UPSIDE_DOWN + "/" + videoStreamName + " live=1";
outputLive = "rtmp://" + destinationIp + "/video/" + meetingId + "/" + videoStreamName;
output = videoStreamName;
ffmpeg = new FFmpegCommand();
ffmpeg.setFFmpegPath(FFMPEG_PATH);
ffmpeg.setInput(input);
ffmpeg.setFormat("flv");
ffmpeg.addRtmpOutputConnectionParameter(meetingId);
ffmpeg.addRtmpOutputConnectionParameter(transcoderId);
ffmpeg.setOutput(outputLive);
ffmpeg.setLoglevel("warning");
ffmpeg.setRotation(FFmpegUtils.ROTATE_UPSIDE_DOWN);
ffmpeg.setAnalyzeDuration("10000"); // 10ms
command = ffmpeg.getFFmpegCommand(true);
break;
default: command = null;
}
if(command != null){
this.ffmpegProcessMonitor = new ProcessMonitor(command,FFMPEG_NAME);
ffmpegProcessMonitor.setProcessMonitorObserver(this);
ffmpegProcessMonitor.start();
return true;
}
return false;
}
private synchronized void stop(){
status = Status.STOPPED;
stopTranscoder();
sendMessage(new StopVideoTranscoderReply(meetingId, transcoderId));
}
private void stopTranscoder() {
if (ffmpegProcessMonitor != null) {
ffmpegProcessMonitor.forceDestroy();
clearData();
}
}
/**
* Clear monitor and ffmpeg data.
*/
private void clearData() {
ffmpegProcessMonitor = null;
ffmpeg = null;
switch (type) {
case TRANSCODE_RTP_TO_RTMP:
FFmpegUtils.removeSDPVideoFile(voiceBridge);
break;
default:
}
}
private synchronized void destroyTranscoder() {
status = Status.STOPPED;
stopTranscoder();
sendMessage(new DestroyVideoTranscoderReply(meetingId, transcoderId));
stopActor();
}
private synchronized void update(Map<String,String> params) {
switch (status) {
case UPDATING:
status = Status.RUNNING;
startTranscoder();
sendMessage(new UpdateVideoTranscoderReply(meetingId, transcoderId, output));
break;
default:
if (params != null) {
String transcoderType = params.get(Constants.TRANSCODER_TYPE);
String input = params.get(Constants.INPUT);
String sourceIp = params.get(Constants.LOCAL_IP_ADDRESS);
String localVideoPort = params.get(Constants.LOCAL_VIDEO_PORT);
String remoteVideoPort = params.get(Constants.REMOTE_VIDEO_PORT);
String destinationIp = params.get(Constants.DESTINATION_IP_ADDRESS);
setType(transcoderType);
setVideoStreamName(input);
setSourceIp(sourceIp);
setLocalVideoPort(localVideoPort);
setRemoteVideoPort(remoteVideoPort);
setDestinationIp(destinationIp);
status = Status.UPDATING; //mark update status
stopTranscoder();
}
break;
}
}
private synchronized void restart() {
if (!maxRestartsReached()) {
System.out.println(" > [Restart] Starting current transcoder " + transcoderId);
status = Status.RUNNING;
lastFFmpegRestartTime = System.currentTimeMillis();
clearData();
startTranscoder();
sendMessage(new RestartVideoTranscoderReply(meetingId, transcoderId, output));
}
}
private boolean maxRestartsReached() {
currentFFmpegRestartNumber++;
if(currentFFmpegRestartNumber == MAX_RESTARTINGS_NUMBER) {
long timeInterval = System.currentTimeMillis() - lastFFmpegRestartTime;
if(timeInterval <= MIN_RESTART_TIME) {
System.out.println(" > Max number of ffmpeg restartings reached in " + timeInterval + " miliseconds for " + transcoderId + "'s Video Transcoder." +
" Not restating it anymore.");
return true;
}
else
currentFFmpegRestartNumber = 0;
}
return false;
}
public synchronized void transcodingFinishedSuccessfully() {
sendMessage(new TranscodingFinishedSuccessfully(meetingId, transcoderId)); //tell parent for clean up
stopActor();
}
public synchronized void probeVideoStream(){
if (ffmpegProcessMonitor != null) {
FFProbeCommand ffprobe = new FFProbeCommand(outputLive);
String command[];
ffprobe.setFFprobepath(FFPROBE_PATH);
ffprobe.setInput(outputLive);
ffprobe.setAnalyzeDuration("1");
ffprobe.setShowStreams();
ffprobe.setLoglevel("quiet");
ffprobe.getFFprobeCommand(true);
command = ffprobe.getFFprobeCommand(true);
if(command != null){
this.ffprobeProcessMonitor = new ProcessMonitor(command,FFPROBE_NAME);
ffprobeProcessMonitor.setProcessMonitorObserver(this);
ffprobeProcessMonitor.start();
}
} else {
}
}
private void updateGlobalStreamName(String streamName){
this.videoStreamName = streamName;
String outputLive;
String[] newCommand;
outputLive = "rtmp://" + destinationIp + "/video/" + meetingId + "/"
+ this.videoStreamName+" live=1";
ffmpeg.setOutput(outputLive); //update ffmpeg's output
newCommand = ffmpeg.getFFmpegCommand(true);
ffmpegProcessMonitor.setCommand(newCommand); //update ffmpeg command
}
public void setVideoTranscoderObserver(VideoTranscoderObserver observer){
this.observer = observer;
}
@Override
public void handleProcessFinishedUnsuccessfully(String processMonitorName,String processOutput) {
if ((processMonitorName == null)|| processMonitorName.isEmpty()){
return;
}
if (FFMPEG_NAME.equals(processMonitorName)){
sendMessage(new TranscodingFinishedUnsuccessfully(meetingId, transcoderId));
}else if (FFPROBE_NAME.equals(processMonitorName)){
System.out.println(" > Failed to probe video stream " + outputLive);
}
}
@Override
public void handleProcessFinishedWithSuccess(String processMonitorName, String processOutput) {
if ((processMonitorName == null)|| processMonitorName.isEmpty()) {
System.out.println(" > Can't handle process process monitor finishing with success: UNKNOWN PROCESS");
return;
}
if (FFMPEG_NAME.equals(processMonitorName)){
switch (status) {
case RUNNING:
System.out.println(" > Transcoder finished with success but wasn't closed by the user " + transcoderId + ". Informing parentActor");
transcodingFinishedSuccessfully();
break;
case STOPPED:
System.out.println(" > Transcoder closed by the user. Finished with success");
break;
case UPDATING:
update(null);
break;
default:
break;
}
}
else if (FFPROBE_NAME.equals(processMonitorName)){
String ffprobeOutput = processOutput;
Map<String,String> ffprobeData = parseFFprobeOutput(ffprobeOutput);
sendMessage(new StartVideoProbingReply(meetingId, transcoderId, ffprobeData));
}else{
System.out.println("Can't handle process monitor finishing with success: UNKNOWN PROCESS");
}
}
public Map<String,String> parseFFprobeOutput(String ffprobeOutput){
Pattern pattern = Pattern.compile("(.*)=(.*)");
Map<String, String> ffprobeResult = new HashMap<String, String>();
BufferedReader buf = new BufferedReader(new StringReader(ffprobeOutput));
String line = null;
try {
while( (line=buf.readLine()) != null){
Matcher matcher = pattern.matcher(line);
if(matcher.matches()) {
ffprobeResult.put(matcher.group(1), matcher.group(2));
}
}
} catch (IOException e){
//log.debug("Error when parsing FFprobe's output");
}
return ffprobeResult;
}
public boolean canFFmpegRun() {
//log.debug("Checking if FFmpeg can run...");
return validateIps() && isFFmpegPathValid();
}
public boolean validateIps(){
if ((sourceIp == null || sourceIp.isEmpty())
&& (type == Type.TRANSCODE_RTMP_TO_RTP))
return false;
if ((destinationIp == null || destinationIp.isEmpty())
&& (type == Type.TRANSCODE_FILE_TO_RTMP
|| type == Type.TRANSCODE_FILE_TO_RTP
|| type == Type.TRANSCODE_RTMP_TO_RTP
|| type == Type.TRANSCODE_FILE_TO_RTP))
return false;
return true;
}
public boolean isFFmpegPathValid() {
/*if (!GlobalCall.ffmpegExists(FFMPEG_PATH)) {
//log.debug("***FFMPEG DOESN'T EXIST: check the FFMPEG path in bigbluebutton-sip.properties");
return false;
}*/
return true;
}
public boolean areRotateParametersValid() {
// TODO: Check parameters
return true;
}
public boolean areH264ToH263ParametersValid() {
// TODO: Check parameters
return true;
}
public boolean areRtmpToRtpParametersValid() {
//log.debug("Checking Rtmp to Rtp Transcoder Parameters...");
if(meetingId == null || meetingId.isEmpty()) {
//log.debug("meetingId is null or empty");
return false;
}
if(videoStreamName == null || videoStreamName.isEmpty()) {
//log.debug("videoStreamName is null or empty");
return false;
}
return areVideoPortsValid();
}
public boolean areRtpToRtmpParametersValid() {
//log.debug("Checking Rtp to Rtmp Transcoder Parameters...");
if(meetingId == null || meetingId.isEmpty()) {
//log.debug("meetingId is null or empty");
return false;
}
return isSdpPathValid();
}
public boolean areFileToRtpParametersValid() {
//log.debug("Checking File to Rtp Transcoder Parameters...");
return areVideoPortsValid() && isVideoConfLogoValid();
}
public boolean areFileToRtmpParametersValid() {
//log.debug("Checking File to Rtmp Transcoder Parameters...");
if(meetingId == null || meetingId.isEmpty()) {
//log.debug("meetingId is null or empty");
return false;
}
return isVideoConfLogoValid();
}
public boolean areVideoPortsValid() {
if(localVideoPort == null || localVideoPort.isEmpty()) {
//log.debug("localVideoPort is null or empty");
return false;
}
if(remoteVideoPort == null || remoteVideoPort.isEmpty()) {
//log.debug("remoteVideoPort is null or empty");
return false;
}
if(localVideoPort.equals("0")) {
//log.debug("localVideoPort is 0");
return false;
}
if(remoteVideoPort.equals("0")) {
//log.debug("remoteVideoPort is 0");
return false;
}
return true;
}
public boolean isVideoConfLogoValid() {
/*if(!GlobalCall.videoConfLogoExists(VIDEO_CONF_LOGO_PATH)) {
//log.debug("***IMAGE FOR VIDEOCONF-LOGO VIDEO DOESN'T EXIST: check the image path in bigbluebutton-sip.properties");
return false;
}*/
return true;
}
public boolean isSdpPathValid() {
/*if(!GlobalCall.sdpVideoExists(sdpPath)) {
//log.debug("***SDP FOR GLOBAL FFMPEG ({}) doesn't exist", sdpPath);
return false;
}*/
return true;
}
public String getVideoStreamName(){
return this.videoStreamName;
}
public String getTranscoderId(){
return this.transcoderId;
}
public String getMeetingId(){
return this.meetingId;
}
public String getOutput(){
return this.output;
}
public String generateVideoStreamName(Type type){
switch(type){
case TRANSCODE_RTP_TO_RTMP:
return FFmpegUtils.GLOBAL_VIDEO_STREAM_NAME_PREFIX + voiceBridge + "_" + System.currentTimeMillis();
case TRANSCODE_FILE_TO_RTMP:
return FFmpegUtils.VIDEOCONF_LOGO_STREAM_NAME_PREFIX + voiceBridge + "_" + System.currentTimeMillis();
default:
return "unknown_stream_name";
}
}
public void setVideoStreamName(String videoStreamName) {
if (videoStreamName != null) this.videoStreamName = videoStreamName;
}
public void setType(String type) {
if (type == null) return;
switch (type){
case Constants.TRANSCODE_RTP_TO_RTMP:
this.type = Type.TRANSCODE_RTP_TO_RTMP;
break;
case Constants.TRANSCODE_RTMP_TO_RTP:
this.type = Type.TRANSCODE_RTMP_TO_RTP;
break;
case Constants.TRANSCODE_FILE_TO_RTP:
this.type = Type.TRANSCODE_FILE_TO_RTP;
break;
case Constants.TRANSCODE_FILE_TO_RTMP:
this.type = Type.TRANSCODE_FILE_TO_RTMP;
break;
case Constants.TRANSCODE_H264_TO_H263:
this.type = Type.TRANSCODE_H264_TO_H263;
break;
case Constants.TRANSCODE_ROTATE_RIGHT:
this.type = Type.TRANSCODE_ROTATE_RIGHT;
break;
case Constants.TRANSCODE_ROTATE_LEFT:
this.type = Type.TRANSCODE_ROTATE_LEFT;
break;
case Constants.TRANSCODE_ROTATE_UPSIDE_DOWN:
this.type = Type.TRANSCODE_ROTATE_UPSIDE_DOWN;
break;
default:
return;
}
}
public String getType() {
switch (type){
case TRANSCODE_RTP_TO_RTMP:
return Constants.TRANSCODE_RTP_TO_RTMP;
case TRANSCODE_RTMP_TO_RTP:
return Constants.TRANSCODE_RTMP_TO_RTP;
case TRANSCODE_FILE_TO_RTP:
return Constants.TRANSCODE_FILE_TO_RTP;
case TRANSCODE_FILE_TO_RTMP:
return Constants.TRANSCODE_FILE_TO_RTMP;
case TRANSCODE_H264_TO_H263:
return Constants.TRANSCODE_H264_TO_H263;
case TRANSCODE_ROTATE_RIGHT:
return Constants.TRANSCODE_ROTATE_RIGHT;
case TRANSCODE_ROTATE_LEFT:
return Constants.TRANSCODE_ROTATE_LEFT;
case TRANSCODE_ROTATE_UPSIDE_DOWN:
return Constants.TRANSCODE_ROTATE_UPSIDE_DOWN;
default:
return "UNKNOWN";
}
}
public void setSourceIp(String sourceIp) {
if (sourceIp != null) this.sourceIp = sourceIp;
}
public String getSourceIp() {
return sourceIp;
}
public void setLocalVideoPort(String localVideoPort) {
if (localVideoPort != null) this.localVideoPort = localVideoPort;
}
public String getLocalVideoPort() {
return localVideoPort;
}
public void setRemoteVideoPort(String remoteVideoPort) {
if (remoteVideoPort != null) this.remoteVideoPort = remoteVideoPort;
}
public String getRemoteVideoPort() {
return remoteVideoPort;
}
public void setDestinationIp(String destinationIp) {
if (destinationIp != null) this.destinationIp = destinationIp;
}
public String getDestinationIp() {
return destinationIp;
}
}

View File

@ -0,0 +1,9 @@
package org.bigbluebutton.transcode.core;
import java.util.Map;
public interface VideoTranscoderObserver {
public void handleTranscodingFinishedUnsuccessfully();
public void handleTranscodingFinishedWithSuccess();
public void handleVideoProbingFinishedWithSuccess(Map<String,String> ffprobeResult);
}

View File

@ -0,0 +1,12 @@
package org.bigbluebutton.transcode.core.api;
import java.util.Map;
public interface ITranscodingInGW {
void startTranscoder(String meetingId, String transcoderId, Map<String, String> params);
void updateTranscoder(String meetingId, String transcoderId, Map<String, String> params);
void stopTranscoder(String meetingId, String transcoderId);
void stopMeetingTranscoders(String meetingId);
void startProbing(String meetingId, String transcoderId, Map<String, String> params);
}

View File

@ -0,0 +1,401 @@
package org.bigbluebutton.transcode.core.ffmpeg;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import java.util.HashMap;
import java.util.Iterator;
public class FFmpegCommand {
private HashMap args;
private HashMap x264Params;
private List<String[]> rtmpInputConnParams;
private List<String[]> rtmpOutputConnParams;
private String[] command;
private String ffmpegPath;
private String input;
private String output;
private Boolean inputLive;
/* Analyze duration is a special parameter that MUST come before the input */
private String analyzeDuration;
private String probeSize;
/* Parameters when the input is a loop image/file */
private String loop;
private String ignoreLoop;
private int frameRate;
private String frameSize;
public FFmpegCommand() {
this.args = new HashMap();
this.x264Params = new HashMap();
this.rtmpInputConnParams = new ArrayList<String[]>();
this.rtmpOutputConnParams = new ArrayList<String[]>();
this.ffmpegPath = null;
this.inputLive = false;
this.loop = null;
this.frameRate = 0;
}
public String[] getFFmpegCommand(boolean shouldBuild) {
if(shouldBuild)
buildFFmpegCommand();
return this.command;
}
public void buildFFmpegCommand() {
List comm = new ArrayList<String>();
if(this.ffmpegPath == null)
this.ffmpegPath = "/usr/local/bin/ffmpeg";
comm.add(this.ffmpegPath);
if (this.inputLive){
comm.add("-re");
}
/* Analyze duration and probesize MUST come before the input */
if(analyzeDuration != null && !analyzeDuration.isEmpty()) {
comm.add("-analyzeduration");
comm.add(analyzeDuration);
}
if(loop != null && !loop.isEmpty()){
comm.add("-loop");
comm.add(loop);
}
if(ignoreLoop != null && !ignoreLoop.isEmpty()){
comm.add("-ignore_loop");
comm.add(ignoreLoop);
}
if(probeSize != null && !probeSize.isEmpty()) {
comm.add("-probesize");
comm.add(probeSize);
}
buildRtmpInput();
comm.add("-i");
comm.add(input);
Iterator argsIter = this.args.entrySet().iterator();
while (argsIter.hasNext()) {
Map.Entry pairs = (Map.Entry)argsIter.next();
comm.add(pairs.getKey());
comm.add(pairs.getValue());
}
if(!x264Params.isEmpty()) {
comm.add("-x264-params");
String params = "";
Iterator x264Iter = this.x264Params.entrySet().iterator();
while (x264Iter.hasNext()) {
Map.Entry pairs = (Map.Entry)x264Iter.next();
String argValue = pairs.getKey() + "=" + pairs.getValue();
params += argValue;
// x264-params are separated by ':'
params += ":";
}
// Remove trailing ':'
params.replaceAll(":+$", "");
comm.add(params);
}
buildRtmpOutput();
comm.add(this.output);
this.command = new String[comm.size()];
comm.toArray(this.command);
}
/**
* Add rtmp parameters (if there are any) to the current input,
* if the input is rtmp.
*/
private void buildRtmpInput() {
if(!rtmpInputConnParams.isEmpty() && isRtmpInput()) {
StringBuilder sb = new StringBuilder();
for (String s[] : rtmpInputConnParams){
sb.append("conn="+s[0]+":"+s[1]+" ");
}
input+=" "+sb.toString().trim();
}
}
/**
* Add rtmp parameters (if there are any) to the current output,
* if the output is rtmp.
*/
private void buildRtmpOutput() {
if(!rtmpOutputConnParams.isEmpty() && isRtmpOutput()) {
StringBuilder sb = new StringBuilder();
for (String s[] : rtmpOutputConnParams){
sb.append("conn="+s[0]+":"+s[1]+" ");
}
output+=" "+sb.toString().trim();
}
}
public void setFFmpegPath(String arg) {
this.ffmpegPath = arg;
}
public void setInput(String arg) {
this.input = arg;
}
public void setInputLive(Boolean live){
this.inputLive = live;
}
public void setOutput(String arg) {
this.output = arg;
}
public void setCodec(String arg) {
this.args.put("-vcodec", arg);
}
public void setLoop(String arg) {
this.loop = arg;
}
public void setRotation(String arg) {
switch (arg) {
case FFmpegUtils.ROTATE_LEFT:
this.args.put("-vf", "transpose=2");
break;
case FFmpegUtils.ROTATE_RIGHT:
this.args.put("-vf", "transpose=1");
break;
case FFmpegUtils.ROTATE_UPSIDE_DOWN:
this.args.put("-vf", "transpose=2,transpose=2");
break;
}
}
/**
* Set ignore loop (valid for GIFs input, only)
* 0: means that input GIF will loop indefinitely
* @param arg
*/
public void setIgnoreLoop(int arg) {
this.ignoreLoop = Integer.toString(arg);
}
public void setLevel(String arg) {
this.args.put("-level", arg);
}
public void setPreset(String arg) {
this.args.put("-preset", arg);
}
public void setProfile(String arg) {
this.args.put("-profile:v", arg);
}
public void setFormat(String arg) {
this.args.put("-f", arg);
}
public void setPayloadType(String arg) {
this.args.put("-payload_type", arg);
}
public void setLoglevel(String arg) {
this.args.put("-loglevel", arg);
}
public void setPixelFormat(String arg){
this.args.put("-pix_fmt", arg);
}
/**
* Set video bitrate, in Kbps.
* @param arg
*/
public void setVideoBitRate(int arg){
this.args.put("-b:v", Integer.toString(arg)+"k");
}
/**
* Set bufsize, in Kb.
* @param arg
*/
public void setBufSize(int arg){
this.args.put("-bufsize", Integer.toString(arg)+"k");
}
/**
* Set maximum bitrate, in Kbps.
* @param arg
*/
public void setMaxRate(int arg){
this.args.put("-maxrate", Integer.toString(arg)+"k");
}
/**
* Set Group of images (GOP)
* @param arg
*/
public void setGop(int arg){
this.args.put("-g", Integer.toString(arg));
}
/**
* Set maximum NAL size, in bytes.
* This option works with libopenh264 encoder,only
* @param arg
*/
public void setMaxNalSize(String arg){
this.args.put("-max_nal_size", arg);
}
/**
* Set slice_mode for libopenh264 encoder.
* @param arg
*/
public void setSliceMode(String arg){
this.args.put("-slice_mode", arg);
}
/**
* Set rtpflags for RTP encoder.
* @param arg
*/
public void setRtpFlags(String arg){
this.args.put("-rtpflags", arg);
}
public void setSliceMaxSize(String arg) {
this.x264Params.put("slice-max-size", arg);
}
public void setMaxKeyFrameInterval(String arg) {
this.x264Params.put("keyint", arg);
}
public void addCustomParameter(String name, String value) {
this.args.put(name, value);
}
/**
* Set how much time FFmpeg should analyze stream
* data to get stream information. Note that this
* affects directly the delay to start the stream.
*
* @param duration Analysis duration
*/
public void setAnalyzeDuration(String duration) {
this.analyzeDuration = duration;
}
/**
* Probe size, in bytes.
* Minimum value: 32
* Default: 5000000
**/
public void setProbeSize(String size) {
this.probeSize = size;
}
/**
* Set frame rate of the input data
* @param value
*/
public void setFrameRate(int value){
if (value>0)
this.args.put("-r",Integer.toString(value));
}
public void setFrameSize(String value){
this.frameSize = value;
}
/**
* Add parameters for rtmp connections.
* The order of parameters is the order they are added
* @param value
*/
public void addRtmpInputConnectionParameter(String value){
//S: String
this.rtmpInputConnParams.add(new String[]{"S", value});
}
/**
* Add parameters for rtmp connections.
* The order of parameters is the order they are added
* @param value
*/
public void addRtmpInputConnectionParameter(boolean value){
//B: Boolean
this.rtmpInputConnParams.add(new String[]{"B", value?"1":"0"});
}
/**
* Add parameters for rtmp connections.
* The order of parameters is the order they are added
* @param value
*/
public void addRtmpInputConnectionParameter(int value){
//N : Number
this.rtmpInputConnParams.add(new String[]{"N", Integer.toString(value)});
}
/**
* Add parameters for rtmp connections.
* The order of parameters is the order they are added
* @param value
*/
public void addRtmpOutputConnectionParameter(String value){
//S: String
this.rtmpOutputConnParams.add(new String[]{"S", value});
}
/**
* Add parameters for rtmp connections.
* The order of parameters is the order they are added
* @param value
*/
public void addRtmpOutputConnectionParameter(boolean value){
//B: Boolean
this.rtmpOutputConnParams.add(new String[]{"B", value?"1":"0"});
}
/**
* Add parameters for rtmp connections.
* The order of parameters is the order they are added
* @param value
*/
public void addRtmpOutputConnectionParameter(int value){
//N : Number
this.rtmpOutputConnParams.add(new String[]{"N", Integer.toString(value)});
}
/**
* Check if the current set intput is rtmp
* @return
*/
private boolean isRtmpInput(){
return input.contains("rtmp");
}
/**
* Check if the current set output is rtmp
* @return
*/
private boolean isRtmpOutput(){
return output.contains("rtmp");
}
}

View File

@ -0,0 +1,45 @@
package org.bigbluebutton.transcode.core.ffmpeg;
public class FFmpegConstants {
//exit codes (obtained from process exit code)
public static final int FATAL_ERROR_CODE = 128;
public static final int EXIT_WITH_SUCCESS_CODE = 0;
public static final int EXIT_WITH_NO_INPUT_CODE = 1;
public static final int EXIT_WITH_SIGKILL_CODE = FATAL_ERROR_CODE + 9;
public static final int ACCEPTABLE_EXIT_CODES[] = {EXIT_WITH_SUCCESS_CODE,EXIT_WITH_SIGKILL_CODE};
//status codes (obtained from sterr/out)
public static final int EXIT_WITH_SUCCESS_STATUS = 0;
public static final int EXIT_WITH_NO_INPUT_STATUS = 1;
public static final int RUNNING_STATUS = 2;
public static final int ACCEPTABLE_EXIT_STATUS[] = {EXIT_WITH_SUCCESS_STATUS,EXIT_WITH_NO_INPUT_STATUS};
//output constants (obtained from verbose stderr/out)
public static String FFMPEG_EXIT_WITH_NO_INPUT_OUTPUT = "Connection timed out";
public static String WIDTH = "width";
public static String HEIGHT = "height";
//Codecs
public static final String CODEC_ID_H264 = "96" ;
public static final String CODEC_NAME_H264 = "H264" ;
public static final String SAMPLE_RATE_H264 = "90000" ;
public static boolean acceptableExitCode(int code){
int i;
if ((ACCEPTABLE_EXIT_CODES == null) || (code < 0)) return false;
for(i=0;i<ACCEPTABLE_EXIT_CODES.length;i++)
if (ACCEPTABLE_EXIT_CODES[i] == code)
return true;
return false;
}
public static boolean acceptableExitStatus(int status){
int i;
if ((ACCEPTABLE_EXIT_STATUS == null) || (status < 0)) return false;
for(i=0;i<ACCEPTABLE_EXIT_STATUS.length;i++)
if (ACCEPTABLE_EXIT_STATUS[i] == status)
return true;
return false;
}
}

View File

@ -0,0 +1,150 @@
package org.bigbluebutton.transcode.core.ffmpeg;
import java.io.BufferedWriter;
import java.io.File;
import java.io.IOException;
import java.nio.charset.Charset;
import java.nio.file.FileSystems;
import java.nio.file.Files;
import java.nio.file.OpenOption;
import java.nio.file.Path;
import java.nio.file.StandardOpenOption;
import org.bigbluebutton.transcode.core.TranscodersService;
public class FFmpegUtils {
private static final String LOW_QUALITY = "160x120";
private static final String MEDIUM_QUALITY = "320x240";
private static final String HIGH_QUALITY = "640x480";
public static final String VIDEO_MODULE = "video";
public static final String DESKSHARE_MODULE = "deskShare";
public static final String ROTATE_RIGHT = "rotate_right";
public static final String ROTATE_LEFT = "rotate_left";
public static final String ROTATE_UPSIDE_DOWN = "rotate_left/rotate_left";
public static final String H263PREFIX = "h263";
public static String defaultVideoWidth;
public static String defaultVideoHeight;
public static final String GLOBAL_VIDEO_STREAM_NAME_PREFIX = "sip_";
public static final String VIDEOCONF_LOGO_STREAM_NAME_PREFIX = "video_conf_";
private static final String sdpVideoFullPath = "/tmp/"+GLOBAL_VIDEO_STREAM_NAME_PREFIX; //when changed , must also change VideoApplication.java in bbb-video
private static OpenOption[] fileOptions = new OpenOption[] {StandardOpenOption.CREATE,StandardOpenOption.WRITE};
public static String ffmpegPath = TranscodersService.ffmpegPath();
public static String ffprobePath = TranscodersService.ffprobePath();
public static String videoconfLogoPath = TranscodersService.videoconfLogoImagePath();
private static boolean enableUserVideoSubtitle = TranscodersService.enableUserVideoSubtitle();
public static String createSDPVideoFile(String userId, String localIpAddress, String localVideoPort, String codecName, String codecId, String sampleRate, String voiceconf) {
Path sdpVideoPath = FileSystems.getDefault().getPath(sdpVideoFullPath + voiceconf+".sdp");
String sdp = "v=0\r\n"
+ "o=" + userId + " 0 0 IN IP4 " + localIpAddress + "\r\n"
+ "s=Session SIP/SDP\r\n"
+ "c=IN IP4 " + localIpAddress + "\r\n"
+ "t=0 0\r\n"
+ "m=video " + localVideoPort + " RTP/AVPF " + codecId +"\r\n"
+ "a=rtpmap:" + codecId + " " + codecName + "/" + sampleRate + "/1\r\n"
+ "a=fmtp:96\r\n"
+ "a=rtcp-fb:" + codecId + " ccm fir \r\n"
+ "a=rtcp-fb:" + codecId + " nack \r\n"
+ "a=rtcp-fb:" + codecId + " nack pli \r\n"
+ "a=rtcp-fb:" + codecId + " goog-remb \r\n";
Charset charset = Charset.forName("US-ASCII");
try {
BufferedWriter writer = Files.newBufferedWriter(sdpVideoPath,charset,fileOptions);
writer.write(sdp, 0, sdp.length());
writer.close();
System.out.println("SDP video file created at: "+sdpVideoPath.toString());
} catch (IOException x) {
System.out.println("Failed to create SDP video file: "+sdpVideoPath.toString());
}
return (sdpVideoPath==null)?null:sdpVideoPath.toString();
}
public static void removeSDPVideoFile(String voiceconf) {
Path sdpVideoPath = FileSystems.getDefault().getPath(sdpVideoFullPath +voiceconf+".sdp");
try {
Files.deleteIfExists(sdpVideoPath);
} catch (IOException e) {
System.out.println("Failed to remove SDP video file: "+sdpVideoPath.toString());
}
}
public String getSdpVideoPath(String voiceconf) {
return sdpVideoFullPath+voiceconf+".sdp";
}
public boolean sdpVideoExists(String sdpFilePath) {
return fileExists(sdpFilePath);
}
private boolean fileExists(String filePath) {
if(filePath == null || filePath.isEmpty())
return false;
return new File(filePath).isFile();
}
public boolean isVideoConfLogoStream(String videoStreamName) {
return ((videoStreamName != null) && (videoStreamName.startsWith(VIDEOCONF_LOGO_STREAM_NAME_PREFIX)));
}
public void setFfmpegPath(String ffPath) {
System.out.println("Trying to set the ffmpeg path to: " + ffPath);
if(ffmpegExists(ffPath)) {
ffmpegPath = ffPath;
System.out.println("ffmpeg path set to: " + ffmpegPath);
}
else
System.out.println("****Could NOT set " + ffPath + " as the ffmpeg path");
}
public boolean ffmpegExists(String ffPath) {
return fileExists(ffPath);
}
public static boolean isUserVideoSubtitleEnabled(){
return enableUserVideoSubtitle;
}
public boolean videoconfLogoExists(String filePath) {
return fileExists(filePath);
}
private static void validateResolution(String resolution) {
System.out.println("Validating sip video resolution: " + resolution);
switch(resolution) {
case LOW_QUALITY:
case MEDIUM_QUALITY:
case HIGH_QUALITY: parseResolution(resolution);
break;
//using the default resolution
default: System.out.println("****The resolution set in bigbluebutton-sip.properties is invalid. Using the default resolution.");
parseResolution(MEDIUM_QUALITY);
}
}
private static void parseResolution(String resolution) {
String[] dimensions = resolution.split("x");
defaultVideoWidth = dimensions[0];
defaultVideoHeight = dimensions[1];
System.out.println("Video Resolution is " + defaultVideoWidth + "x" + defaultVideoHeight);
}
public static String getVideoWidth() {
System.out.println("Getting video width: " + defaultVideoWidth + " (Resolution is " + defaultVideoWidth + "x" + defaultVideoHeight + ")");
return defaultVideoWidth;
}
public static String getVideoHeight() {
System.out.println("Getting video height: " + defaultVideoHeight + " (Resolution is " + defaultVideoWidth + "x" + defaultVideoHeight + ")");
return defaultVideoHeight;
}
}

View File

@ -0,0 +1,95 @@
package org.bigbluebutton.transcode.core.ffprobe;
import java.lang.Runtime;
import java.util.ArrayList;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.HashMap;
import java.util.regex.Pattern;
import java.util.regex.Matcher;
import java.io.InputStreamReader;
import java.io.BufferedReader;
import java.io.IOException;
public class FFProbeCommand {
private String input;
private String[] command;
private String ffprobePath;
private HashMap args;
/* Analyze duration is a special parameter that MUST come before the input */
private String analyzeDuration;
public FFProbeCommand(String input) {
this.input = input;
this.command = null;
this.ffprobePath = null;
this.args = new HashMap();
}
public String[] getFFprobeCommand(boolean shouldBuild){
if(shouldBuild)
buildFFprobeCommand();
return this.command;
}
public void buildFFprobeCommand() {
List comm = new ArrayList<String>();
if(this.ffprobePath == null)
this.ffprobePath = "/usr/local/bin/ffprobe";
comm.add(this.ffprobePath);
/* Analyze duration MUST come before the input */
if(analyzeDuration != null && !analyzeDuration.isEmpty()) {
comm.add("-analyzeduration");
comm.add(analyzeDuration);
}
comm.add("-i");
comm.add(input);
Iterator argsIter = this.args.entrySet().iterator();
while (argsIter.hasNext()) {
Map.Entry pairs = (Map.Entry)argsIter.next();
comm.add(pairs.getKey());
if (pairs.getValue()!=null)
comm.add(pairs.getValue());
}
this.command = new String[comm.size()];
comm.toArray(this.command);
}
public void setFFprobepath(String arg) {
this.ffprobePath = arg;
}
public void setAnalyzeDuration(String duration) {
this.analyzeDuration = duration;
}
public void setInput(String arg){
this.input = arg;
}
public void setLoglevel(String arg){
this.args.put("-loglevel", arg);
}
public void setShowStreams(){
this.args.put("-show_streams",null);
}
public void selectStream(String arg){
this.args.put("-select_streams", arg);
}
}

View File

@ -0,0 +1,255 @@
package org.bigbluebutton.transcode.core.processmonitor;
import java.io.InputStream;
import java.io.IOException;
import java.lang.reflect.Field;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import org.bigbluebutton.transcode.core.ffmpeg.FFmpegConstants;
public class ProcessMonitor {
private String[] command;
private Process process;
private String name;
ProcessStream inputStreamMonitor;
ProcessStream errorStreamMonitor;
private String inputStreamMonitorOutput;
private String errorStreamMonitorOutput;
public static enum Status{RUNNING,CLOSED_BY_USER};
private Status status;
private Thread thread;
private ProcessMonitorObserver observer;
public ProcessMonitor(String[] command,String name) {
this.command = command;
this.process = null;
this.thread = null;
this.inputStreamMonitor = null;
this.errorStreamMonitor = null;
this.name = name;
this.inputStreamMonitorOutput = null;
this.errorStreamMonitor = null;
}
@Override
public String toString() {
if (this.command == null || this.command.length == 0) {
return "";
}
Pattern pattern = Pattern.compile("(.*) (.*)");
StringBuffer result = new StringBuffer();
String delim = "";
for (String i : this.command) {
Matcher matcher = pattern.matcher(i);
if(matcher.matches()) {
result.append(delim).append("\""+matcher.group(1)+" "+matcher.group(2)+"\"");
}else result.append(delim).append(i);
delim = " ";
}
return removeLogLevelFlag(result.toString());
}
private String getCommandString(){
//used by the process's thread instead of toString()
return this.toString();
}
public void setCommand(String[] command){
this.command = command;
}
private void notifyProcessMonitorObserverOnFinishedUnsuccessfully() {
if(observer != null){
//log.debug("Notifying ProcessMonitorObserver that process finished unsuccessfully");
observer.handleProcessFinishedUnsuccessfully(this.name,inputStreamMonitorOutput);
}else {
//log.debug("Cannot notify ProcessMonitorObserver that process finished unsuccessfully: ProcessMonitorObserver null");
}
}
private void notifyProcessMonitorObserverOnFinished() {
if(observer != null){
//log.debug("Notifying ProcessMonitorObserver that {} successfully finished",this.name);
observer.handleProcessFinishedWithSuccess(this.name,inputStreamMonitorOutput);
}else {
//log.debug("Cannot notify ProcessMonitorObserver that {} finished: ProcessMonitorObserver null",this.name);
}
}
public synchronized void start() {
if(this.thread == null){
this.thread = new Thread( new Runnable(){
public void run(){
try {
System.out.println(" > Creating thread to execute " + name);
process = Runtime.getRuntime().exec(command);
System.out.println(" > Executing " + name + "( pid=" + getPid() + " ):\n " + getCommandString());
if(status == Status.CLOSED_BY_USER) {
System.out.println(" > ProcessMonitor closed by user. Closing " + name + " it immediatelly");
forceDestroy();
return;
}
InputStream is = process.getInputStream();
InputStream es = process.getErrorStream();
inputStreamMonitor = new ProcessStream(is,"STDOUT");
errorStreamMonitor = new ProcessStream(es,"STDERR");
inputStreamMonitor.start();
errorStreamMonitor.start();
process.waitFor();
}
catch(SecurityException se) {
System.out.println("Security Exception");
}
catch(IOException ioe) {
System.out.println("IO Exception");
}
catch(NullPointerException npe) {
System.out.println("NullPointer Exception");
}
catch(IllegalArgumentException iae) {
System.out.println("IllegalArgument Exception");
}
catch(InterruptedException ie) {
System.out.println("Interrupted Exception");
}
int ret = process.exitValue();
if (FFmpegConstants.acceptableExitCode(ret) || errorStreamMonitor.acceptableExitStatus()){
//log.debug("Exiting thread that executes {}. Exit value: {}, Exit status (from stdout): {} ",name,ret, errorStreamMonitor.getExitStatus());
storeProcessOutputs(inputStreamMonitor.getOutput(), errorStreamMonitor.getOutput());
clearData();
notifyProcessMonitorObserverOnFinished();
}
else{
//log.debug("Exiting thread that executes {}. Exit value: {}, Exit status (from stdout): {}",name,ret,errorStreamMonitor.getExitStatus());
storeProcessOutputs(inputStreamMonitor.getOutput(), errorStreamMonitor.getOutput());
clearData();
notifyProcessMonitorObserverOnFinishedUnsuccessfully();
}
}
});
status = Status.RUNNING;
this.thread.start();
}else{
//log.debug("Can't start a new process monitor: It is already running.");
}
}
public synchronized void restart(){
clearData();
status = Status.CLOSED_BY_USER;
start();
}
private void clearData(){
closeProcessStream();
closeProcess();
clearMonitorThread();
}
private void clearMonitorThread(){
if (this.thread !=null)
this.thread=null;
}
private void closeProcessStream(){
if(this.inputStreamMonitor != null){
this.inputStreamMonitor.close();
this.inputStreamMonitor = null;
}
if (this.errorStreamMonitor != null) {
this.errorStreamMonitor.close();
this.errorStreamMonitor = null;
}
}
private void closeProcess(){
if(this.process != null) {
status = Status.CLOSED_BY_USER;
//log.debug("Closing {} process",this.name);
this.process.destroy();
this.process = null;
}
}
private void storeProcessOutputs(String inputStreamOutput,String errorStreamOutput){
this.inputStreamMonitorOutput = inputStreamOutput;
this.errorStreamMonitorOutput = errorStreamOutput;
}
public synchronized void destroy() {
if (this.thread != null){
status = Status.CLOSED_BY_USER;
clearData();
//log.debug("ProcessMonitor successfully finished");
}else{
//log.debug("Can't destroy this process monitor: There's no process running.");
}
}
public void setProcessMonitorObserver(ProcessMonitorObserver observer){
if (observer==null){
//log.debug("Cannot assign observer: ProcessMonitorObserver null");
}else this.observer = observer;
}
public int getPid(){
Field f;
int pid;
try {
if (this.process == null) return -1;
f = this.process.getClass().getDeclaredField("pid");
f.setAccessible(true);
pid = (int)f.get(this.process);
return pid;
} catch (IllegalArgumentException | IllegalAccessException
| NoSuchFieldException | SecurityException e) {
//log.debug("Error when obtaining {} PID",this.name);
return -1;
}
}
public synchronized void forceDestroy(){
if (this.thread != null) {
status = Status.CLOSED_BY_USER;
try {
int pid = getPid();
if (pid < 0){
//log.debug("Process doesn't exist. Not destroying it...");
return;
}else {
Runtime.getRuntime().exec("kill -9 "+ getPid());
}
} catch (IOException e) {
//log.debug("Failed to force-kill {} process",this.name);
e.printStackTrace();
}
}else{
//log.debug("Can't force-destroy this process monitor: There's no process running.");
}
}
public boolean isFFmpegProcess(){
return this.name.toLowerCase().contains("ffmpeg");
}
/**
* Removes loglevel flag of ffmpeg command.
* Usefull for faster debugging
*/
private String removeLogLevelFlag(String commandString){
if (isFFmpegProcess()){
return commandString.replaceAll("-loglevel \\w+", "");
}else return commandString;
}
}

View File

@ -0,0 +1,6 @@
package org.bigbluebutton.transcode.core.processmonitor;
public interface ProcessMonitorObserver {
public void handleProcessFinishedUnsuccessfully(String processName, String processOutput);
public void handleProcessFinishedWithSuccess(String processName, String processOutput);
}

View File

@ -0,0 +1,95 @@
package org.bigbluebutton.transcode.core.processmonitor;
import java.io.BufferedReader;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.io.IOException;
import org.bigbluebutton.transcode.core.ffmpeg.FFmpegConstants;
public class ProcessStream {
private InputStream stream;
private Thread thread;
private String type;
private String output;
private int exitStatus = FFmpegConstants.RUNNING_STATUS;
ProcessStream(InputStream stream, String type) {
if(stream != null)
this.stream = stream;
this.type = type;
this.output = "";
}
protected void start() {
exitStatus = FFmpegConstants.RUNNING_STATUS;
this.thread = new Thread( new Runnable(){
public void run(){
try {
String line;
InputStreamReader isr = new InputStreamReader(stream);
BufferedReader ibr = new BufferedReader(isr);
output = "";
while ((line = ibr.readLine()) != null) {
////log.debug("[{}]"+line,type);
updateCurrentStatusFromOutput(line);
output+=line+"\n";
}
close();
}
catch(IOException ioe) {
//log.debug("Finishing process stream [type={}] because there's no more data to be read",type);
close();
}
}
});
this.thread.start();
}
protected void close() {
try {
if(this.stream != null) {
////log.debug("Closing process stream");
this.stream.close();
this.stream = null;
}
}
catch(IOException ioe) {
//log.debug("IOException");
}
}
protected String getOutput(){
return this.output;
}
/**
* Update current process status based on the stdout.
* The exitStatus is mapped according to ffmpeg exit status
* @param outputLine
* Requires loglevel verbose of FFmpegCommand
*/
private void updateCurrentStatusFromOutput(String outputLine){
if (outputLine != null){
if (outputLine.contains(FFmpegConstants.FFMPEG_EXIT_WITH_NO_INPUT_OUTPUT)){
////log.debug("FFmpeg exited with no input status.");
exitStatus = FFmpegConstants.EXIT_WITH_NO_INPUT_STATUS;
}
/*else if outputLine.contains(FFmpegConstants....)
exitStatus = FFmpegConstants....
*/
}
}
public int getExitStatus(){
return exitStatus;
}
/**
* Validates exit status
*/
public boolean acceptableExitStatus(){
return FFmpegConstants.acceptableExitStatus(exitStatus);
}
}

View File

@ -0,0 +1,81 @@
package org.bigbluebutton.transcode.pubsub.receivers;
import org.bigbluebutton.transcode.core.api.ITranscodingInGW;
import org.bigbluebutton.common.messages.StartTranscoderRequestMessage;
import org.bigbluebutton.common.messages.UpdateTranscoderRequestMessage;
import org.bigbluebutton.common.messages.StopTranscoderRequestMessage;
import org.bigbluebutton.common.messages.StopMeetingTranscodersMessage;
import org.bigbluebutton.common.messages.StartProbingRequestMessage;
import com.google.gson.JsonObject;
import com.google.gson.JsonParser;
public class RedisMessageReceiver {
public static final String TO_BBB_TRANSCODE_CHANNEL = "bigbluebutton:to-bbb-transcode";
public static final String TO_BBB_TRANSCODE_PATTERN = TO_BBB_TRANSCODE_CHANNEL + ":*";
public static final String TO_BBB_TRANSCODE_SYSTEM_CHAN = TO_BBB_TRANSCODE_CHANNEL + ":system";
private ITranscodingInGW transcodingInGW;
public RedisMessageReceiver(ITranscodingInGW transcodingInGW) {
this.transcodingInGW = transcodingInGW;
}
public void handleMessage(String pattern, String channel, String message) {
if (channel.equalsIgnoreCase(TO_BBB_TRANSCODE_SYSTEM_CHAN)) {
JsonParser parser = new JsonParser();
JsonObject obj = (JsonObject) parser.parse(message);
if (obj.has("header") && obj.has("payload")) {
JsonObject header = (JsonObject) obj.get("header");
if (header.has("name")) {
String messageName = header.get("name").getAsString();
switch (messageName) {
case StartTranscoderRequestMessage.START_TRANSCODER_REQUEST:
processStartTranscoderRequestMessage(message);
break;
case UpdateTranscoderRequestMessage.UPDATE_TRANSCODER_REQUEST:
processUpdateTranscoderRequestMessage(message);
break;
case StopTranscoderRequestMessage.STOP_TRANSCODER_REQUEST:
processStopTranscoderRequestMessage(message);
break;
case StopMeetingTranscodersMessage.STOP_MEETING_TRANSCODERS:
processStopMeetingTranscodersMessage(message);
break;
case StartProbingRequestMessage.START_PROBING_REQUEST:
processStartProbingRequestMessage(message);
}
}
}
}
}
private void processStartTranscoderRequestMessage(String json) {
StartTranscoderRequestMessage msg = StartTranscoderRequestMessage.fromJson(json);
transcodingInGW.startTranscoder(msg.meetingId, msg.transcoderId, msg.params);
}
private void processUpdateTranscoderRequestMessage(String json) {
UpdateTranscoderRequestMessage msg = UpdateTranscoderRequestMessage.fromJson(json);
transcodingInGW.updateTranscoder(msg.meetingId, msg.transcoderId, msg.params);
}
private void processStopTranscoderRequestMessage(String json) {
StopTranscoderRequestMessage msg = StopTranscoderRequestMessage.fromJson(json);
transcodingInGW.stopTranscoder(msg.meetingId, msg.transcoderId);
}
private void processStopMeetingTranscodersMessage(String json) {
StopMeetingTranscodersMessage msg = StopMeetingTranscodersMessage.fromJson(json);
transcodingInGW.stopMeetingTranscoders(msg.meetingId);
}
private void processStartProbingRequestMessage(String json) {
StartProbingRequestMessage msg = StartProbingRequestMessage.fromJson(json);
transcodingInGW.startProbing(msg.meetingId, msg.transcoderId, msg.params);
}
}

View File

@ -0,0 +1,45 @@
akka {
actor {
debug {
receive = on
}
}
loglevel = INFO
stdout-loglevel = "INFO"
rediscala-subscriber-worker-dispatcher {
mailbox-type = "akka.dispatch.SingleConsumerOnlyUnboundedMailbox"
# Throughput defines the maximum number of messages to be
# processed per actor before the thread jumps to the next actor.
# Set to 1 for as fair as possible.
throughput = 512
}
}
redis {
host="127.0.0.1"
port=6379
password=""
}
videoconference {
#The image to use in the videoconference window and/or when the webuser has no video
videoconf-logo-image-path = /usr/share/red5/webapps/sip/WEB-INF/mconf-videoconf-logo.gif
#Enable username subtitle on video-conf-logo (the one shown in sip-phone when
#webconference's talker has no video )
enable-user-video-subtitle = true
#To change the sip video resolution, edit below:
#IMPORTANT: For now, we only accept these 3 resolutions: 160x120, 320x240, 640x480
sip-video-resolution=640x480
}
transcoder {
#The path where ffmpeg is installed
ffmpeg-path = /usr/bin/ffmpeg
#The path where ffprobe is installed
ffprobe-path = /usr/bin/ffprobe
}

View File

@ -0,0 +1,29 @@
<?xml version="1.0" encoding="UTF-8"?>
<configuration>
<appender name="STDOUT" class="ch.qos.logback.core.ConsoleAppender">
<encoder>
<pattern>%date{ISO8601} %-5level %logger{36} %X{akkaSource} - %msg%n</pattern>
</encoder>
</appender>
<appender name="FILE" class="ch.qos.logback.core.rolling.RollingFileAppender">
<File>logs/bbb-transcode.log</File>
<rollingPolicy class="ch.qos.logback.core.rolling.TimeBasedRollingPolicy">
<FileNamePattern>logs/bbb-transcode.%d{yyyy-MM-dd}.log</FileNamePattern>
<!-- keep 30 days worth of history -->
<MaxHistory>5</MaxHistory>
</rollingPolicy>
<layout class="ch.qos.logback.classic.PatternLayout">
<Pattern>%d{"yyyy-MM-dd HH:mm:ss,SSSXXX"} [%thread] %-5level %logger{35} - %msg%n</Pattern>
</layout>
</appender>
<logger name="akka" level="INFO" />
<logger name="org.bigbluebutton" level="DEBUG" />
<logger name="org.freeswitch.transcode" level="WARN" />
<root level="INFO">
<appender-ref ref="STDOUT"/>
<appender-ref ref="FILE" />
</root>
</configuration>

View File

@ -0,0 +1,23 @@
package org.bigbluebutton
import akka.actor.{ ActorSystem, Props }
import scala.concurrent.duration._
import redis.RedisClient
import scala.concurrent.{ Future, Await }
import scala.concurrent.ExecutionContext.Implicits.global
import org.bigbluebutton.endpoint.redis.{ RedisPublisher, AppsRedisSubscriberActor }
import org.bigbluebutton.transcode.pubsub.receivers.RedisMessageReceiver
import org.bigbluebutton.transcode.core.TranscodingInGW
object Boot extends App with SystemConfiguration {
implicit val system = ActorSystem("bigbluebutton-transcode-system")
val redisPublisher = new RedisPublisher(system)
var transcodingInGW = new TranscodingInGW(system, redisPublisher);
val redisMsgReceiver = new RedisMessageReceiver(transcodingInGW);
val redisSubscriberActor = system.actorOf(AppsRedisSubscriberActor.props(system, redisMsgReceiver), "redis-subscriber")
}

View File

@ -0,0 +1,20 @@
package org.bigbluebutton
import com.typesafe.config.ConfigFactory
import scala.util.Try
trait SystemConfiguration {
val config = ConfigFactory.load()
lazy val redisHost = Try(config.getString("redis.host")).getOrElse("127.0.0.1")
lazy val redisPort = Try(config.getInt("redis.port")).getOrElse(6379)
lazy val redisPassword = Try(config.getString("redis.password")).getOrElse("")
lazy val _ffmpegPath = Try(config.getString("transcoder.ffmpeg-path")).getOrElse("/usr/local/bin/ffmpeg")
lazy val _ffprobePath = Try(config.getString("transcoder.ffprobe-path")).getOrElse("/usr/local/bin/ffprobe")
lazy val _videoconfLogoImagePath = Try(config.getString("videoconference.videoconf-logo-image-path")).getOrElse("")
lazy val _enableUserVideoSubtitle = Try(config.getString("videoconference.enable-user-video-subtitle").toBoolean).getOrElse(false)
lazy val _sipVideoResolution = Try(config.getString("videoconference.sip-video-resolution")).getOrElse("")
}

View File

@ -0,0 +1,79 @@
package org.bigbluebutton.endpoint.redis
import akka.actor.Props
import java.net.InetSocketAddress
import redis.actors.RedisSubscriberActor
import redis.api.pubsub.{ PMessage, Message }
import scala.concurrent.duration._
import akka.actor.ActorRef
import akka.actor.actorRef2Scala
import org.bigbluebutton.SystemConfiguration
import org.bigbluebutton.transcode.pubsub.receivers.RedisMessageReceiver
import redis.api.servers.ClientSetname
import org.bigbluebutton.common.converters.FromJsonDecoder
import org.bigbluebutton.common.messages.PubSubPongMessage
import akka.actor.ActorSystem
import scala.concurrent.duration._
import scala.concurrent.ExecutionContext.Implicits.global
object AppsRedisSubscriberActor extends SystemConfiguration {
val channels = Seq("time")
val patterns = Seq("bigbluebutton:to-bbb-transcode:*")
def props(system: ActorSystem, msgReceiver: RedisMessageReceiver): Props =
Props(classOf[AppsRedisSubscriberActor], system, msgReceiver,
redisHost, redisPort,
channels, patterns).withDispatcher("akka.rediscala-subscriber-worker-dispatcher")
}
class AppsRedisSubscriberActor(val system: ActorSystem, msgReceiver: RedisMessageReceiver, redisHost: String,
redisPort: Int, channels: Seq[String] = Nil, patterns: Seq[String] = Nil)
extends RedisSubscriberActor(new InetSocketAddress(redisHost, redisPort),
channels, patterns) {
val decoder = new FromJsonDecoder()
var lastPongReceivedOn = 0L
system.scheduler.schedule(10 seconds, 10 seconds)(checkPongMessage())
// Set the name of this client to be able to distinguish when doing
// CLIENT LIST on redis-cli
write(ClientSetname("BbbTranscodeAkkaSub").encodedRequest)
def checkPongMessage() {
val now = System.currentTimeMillis()
if (lastPongReceivedOn != 0 && (now - lastPongReceivedOn > 30000)) {
log.error("BBB-Transcode pubsub error!");
}
}
def onMessage(message: Message) {
log.debug(s"message received: $message")
}
def onPMessage(pmessage: PMessage) {
log.debug(s"pattern message received: $pmessage")
val msg = decoder.decodeMessage(pmessage.data)
if (msg != null) {
msg match {
case m: PubSubPongMessage => {
if (m.payload.system == "BbbTranscode") {
lastPongReceivedOn = System.currentTimeMillis()
}
}
case _ => // do nothing
}
} else {
msgReceiver.handleMessage(pmessage.patternMatched, pmessage.channel, pmessage.data)
}
}
def handleMessage(msg: String) {
log.warning("**** TODO: Handle pubsub messages. ****")
}
}

View File

@ -0,0 +1,34 @@
package org.bigbluebutton.endpoint.redis
import akka.actor.Props
import redis.RedisClient
import scala.concurrent.duration._
import scala.concurrent.ExecutionContext.Implicits.global
import akka.actor.ActorSystem
import scala.concurrent.Await
import akka.actor.Actor
import org.bigbluebutton.SystemConfiguration
import org.bigbluebutton.common.converters.ToJsonEncoder
class RedisPublisher(val system: ActorSystem) extends SystemConfiguration {
val redis = RedisClient(redisHost, redisPort)(system)
// Set the name of this client to be able to distinguish when doing
// CLIENT LIST on redis-cli
redis.clientSetname("BbbTranscodeAkkaPub")
val encoder = new ToJsonEncoder()
def sendPingMessage() {
val json = encoder.encodePubSubPingMessage("BbbTranscode", System.currentTimeMillis())
redis.publish("bigbluebutton:to-bbb-apps:system", json)
}
system.scheduler.schedule(10 seconds, 10 seconds)(sendPingMessage())
def publish(channel: String, data: String) {
//println("PUBLISH TO [" + channel + "]: \n [" + data + "]")
redis.publish(channel, data)
}
}

View File

@ -0,0 +1,35 @@
package org.bigbluebutton.transcode.core
import org.bigbluebutton.transcode.core.api.ITranscodingInGW
import org.bigbluebutton.endpoint.redis.RedisPublisher
import scala.collection.JavaConversions._
import java.util.ArrayList
import scala.collection.mutable.ArrayBuffer
import akka.actor.ActorSystem
import org.bigbluebutton.transcode.api._
class TranscodingInGW(val system: ActorSystem, messageSender: RedisPublisher) extends ITranscodingInGW {
val log = system.log
val transcodingActor = system.actorOf(TranscodingActor.props(system, messageSender), "bbb-transcoding-manager")
def startTranscoder(meetingId: String, transcoderId: String, params: java.util.Map[String, String]) {
transcodingActor ! new StartTranscoderRequest(meetingId, transcoderId, params)
}
def updateTranscoder(meetingId: String, transcoderId: String, params: java.util.Map[String, String]) {
transcodingActor ! new UpdateTranscoderRequest(meetingId, transcoderId, params)
}
def stopTranscoder(meetingId: String, transcoderId: String) {
transcodingActor ! new StopTranscoderRequest(meetingId, transcoderId)
}
def stopMeetingTranscoders(meetingId: String) {
transcodingActor ! new StopMeetingTranscoders(meetingId)
}
def startProbing(meetingId: String, transcoderId: String, params: java.util.Map[String, String]) {
transcodingActor ! new StartProbingRequest(meetingId, transcoderId, params)
}
}

View File

@ -0,0 +1,9 @@
package org.bigbluebutton.transcode.api
trait InMessage { val meetingId: String }
case class StartTranscoderRequest(meetingId: String, transcoderId: String, params: java.util.Map[String, String]) extends InMessage
case class UpdateTranscoderRequest(meetingId: String, transcoderId: String, params: java.util.Map[String, String]) extends InMessage
case class StopTranscoderRequest(meetingId: String, transcoderId: String) extends InMessage
case class StopMeetingTranscoders(meetingId: String) extends InMessage
case class StartProbingRequest(meetingId: String, transcoderId: String, params: java.util.Map[String, String]) extends InMessage

View File

@ -0,0 +1,12 @@
package org.bigbluebutton.transcode.api
import scala.collection.mutable.HashMap
abstract class OutMessage
case class StartTranscoderReply(meetingId: String, transcoderId: String, params: HashMap[String, String]) extends OutMessage
case class StopTranscoderReply(meetingId: String, transcoderId: String) extends OutMessage
case class UpdateTranscoderReply(meetingId: String, transcoderId: String, params: HashMap[String, String]) extends OutMessage
case class StartProbingReply(meetingId: String, transcoderId: String, params: HashMap[String, String]) extends OutMessage
case class TranscoderStatusUpdate(meetingId: String, transcoderId: String, params: HashMap[String, String]) extends OutMessage

View File

@ -0,0 +1,84 @@
package org.bigbluebutton.transcode.core
import akka.actor.Actor
import akka.actor.ActorContext
import akka.actor.ActorLogging
import akka.actor.Props
import org.bigbluebutton.transcode.api._
import org.bigbluebutton.endpoint.redis.RedisPublisher
import collection.JavaConverters._
import scala.collection.JavaConversions._
import org.bigbluebutton.common.messages.StartTranscoderReplyMessage
import org.bigbluebutton.common.messages.StopTranscoderReplyMessage
import org.bigbluebutton.common.messages.TranscoderStatusUpdateMessage
import org.bigbluebutton.common.messages.UpdateTranscoderReplyMessage
import org.bigbluebutton.common.messages.StartProbingReplyMessage
import org.bigbluebutton.common.messages.MessagingConstants
object MessageSenderActor {
def props(msgSender: RedisPublisher): Props =
Props(classOf[MessageSenderActor], msgSender)
}
class MessageSenderActor(val msgSender: RedisPublisher)
extends Actor with ActorLogging {
def receive = {
case msg: StartTranscoderReply => handleStartTranscoderReply(msg)
case msg: StopTranscoderReply => handleStopTranscoderReply(msg)
case msg: UpdateTranscoderReply => handleUpdateTranscoderReply(msg)
case msg: TranscoderStatusUpdate => handleTranscoderStatusUpdate(msg)
case msg: StartProbingReply => handleStartProbingReply(msg)
case _ => // do nothing
}
private def handleStartTranscoderReply(msg: StartTranscoderReply) {
System.out.println("Sending StartTranscoderReplyMessage. Params: [\n"
+ "meetingId = " + msg.meetingId + "\n"
+ "transcoderId = " + msg.transcoderId + "\n"
+ "params = " + msg.params.mkString(", ") + "\n]\n")
val str = new StartTranscoderReplyMessage(msg.meetingId, msg.transcoderId, msg.params)
msgSender.publish(MessagingConstants.FROM_BBB_TRANSCODE_SYSTEM_CHAN, str.toJson())
}
private def handleStopTranscoderReply(msg: StopTranscoderReply) {
System.out.println("Sending StopTranscoderReplyMessage. Params: [\n"
+ "meetingId = " + msg.meetingId + "\n"
+ "transcoderId = " + msg.transcoderId + "\n]\n")
val str = new StopTranscoderReplyMessage(msg.meetingId, msg.transcoderId)
msgSender.publish(MessagingConstants.FROM_BBB_TRANSCODE_SYSTEM_CHAN, str.toJson())
}
private def handleUpdateTranscoderReply(msg: UpdateTranscoderReply) {
System.out.println("Sending UpdateTranscoderReplyMessage. Params: [\n"
+ "meetingId = " + msg.meetingId + "\n"
+ "transcoderId = " + msg.transcoderId + "\n"
+ "params = " + msg.params.mkString(", ") + "\n]\n")
val str = new UpdateTranscoderReplyMessage(msg.meetingId, msg.transcoderId, msg.params)
msgSender.publish(MessagingConstants.FROM_BBB_TRANSCODE_SYSTEM_CHAN, str.toJson())
}
private def handleTranscoderStatusUpdate(msg: TranscoderStatusUpdate) {
System.out.println("Sending TranscoderStatusUpdateMessage. Params: [\n"
+ "meetingId = " + msg.meetingId + "\n"
+ "transcoderId = " + msg.transcoderId + "\n"
+ "params = " + msg.params.mkString(", ") + "\n]\n")
val str = new TranscoderStatusUpdateMessage(msg.meetingId, msg.transcoderId, msg.params)
msgSender.publish(MessagingConstants.FROM_BBB_TRANSCODE_SYSTEM_CHAN, str.toJson())
}
private def handleStartProbingReply(msg: StartProbingReply) {
System.out.println("Sending StartProbingReplyMessage. Params: [\n"
+ "meetingId = " + msg.meetingId + "\n"
+ "transcoderId = " + msg.transcoderId + "\n"
+ "params = " + msg.params.mkString(", ") + "\n]\n")
val str = new TranscoderStatusUpdateMessage(msg.meetingId, msg.transcoderId, msg.params)
msgSender.publish(MessagingConstants.FROM_BBB_TRANSCODE_SYSTEM_CHAN, str.toJson())
}
}

View File

@ -0,0 +1,41 @@
package org.bigbluebutton.transcode.core
import scala.collection.mutable.HashMap
import akka.actor.ActorRef
class TranscodersModel {
private var meetings = new HashMap[String, HashMap[String, ActorRef]]
def addTranscoder(meetingId: String, transcoderId: String, transcoderActor: ActorRef) {
meetings.get(meetingId) match {
case Some(transcoders) => transcoders += transcoderId -> transcoderActor
case _ =>
var transcoders = new HashMap[String, ActorRef]
transcoders += transcoderId -> transcoderActor
meetings += meetingId -> transcoders
}
}
def removeTranscoder(meetingId: String, transcoderId: String) {
meetings.get(meetingId) match {
case Some(transcoders) => transcoders -= transcoderId
case _ =>
}
}
def getTranscoder(meetingId: String, transcoderId: String): Option[ActorRef] = {
meetings.get(meetingId) match {
case Some(transcoders) => transcoders.get(transcoderId)
case _ => None
}
}
def getTranscoders(meetingId: String): Array[ActorRef] = {
meetings.get(meetingId) match {
case Some(transcoders) => transcoders.values toArray
case _ => Array.empty
}
}
}

View File

@ -0,0 +1,29 @@
package org.bigbluebutton.transcode.core
import org.bigbluebutton.SystemConfiguration
import org.bigbluebutton.transcode.core.ffmpeg.FFmpegUtils
class TranscodersService {}
object TranscodersService extends SystemConfiguration {
def ffmpegPath(): String = {
_ffmpegPath
}
def ffprobePath(): String = {
_ffprobePath
}
def videoconfLogoImagePath(): String = {
_videoconfLogoImagePath
}
def enableUserVideoSubtitle(): Boolean = {
_enableUserVideoSubtitle
}
def sipVideoResolution(): String = {
_sipVideoResolution
}
}

View File

@ -0,0 +1,116 @@
package org.bigbluebutton.transcode.core
import akka.actor._
import akka.actor.ActorLogging
import scala.collection.mutable.HashMap
import org.bigbluebutton.endpoint.redis.RedisPublisher
import org.bigbluebutton.transcode.api._
import org.bigbluebutton.SystemConfiguration
import scala.collection._
import scala.collection.JavaConversions._
import org.bigbluebutton.common.messages.Constants
import org.bigbluebutton.transcode.core.apps.{ TranscodingObserverApp }
object TranscodingActor extends SystemConfiguration {
def props(system: ActorSystem, messageSender: RedisPublisher): Props =
Props(classOf[TranscodingActor], system, messageSender)
}
class TranscodingActor(val system: ActorSystem, messageSender: RedisPublisher)
extends Actor with ActorLogging with TranscodingObserverApp {
val transcodersModel = new TranscodersModel()
val messageSenderActor = context.actorOf(MessageSenderActor.props(messageSender), "bbb-sender-actor")
def receive = {
case msg: StartTranscoderRequest => handleStartTranscoderRequest(msg)
case msg: UpdateTranscoderRequest => handleUpdateTranscoderRequest(msg)
case msg: StopTranscoderRequest => handleStopTranscoderRequest(msg)
case msg: StopMeetingTranscoders => handleStopMeetingTranscoders(msg)
case msg: StartProbingRequest => handleStartProbingRequest(msg)
//internal messages
case msg: StartVideoTranscoderReply => handleStartVideoTranscoderReply(msg)
case msg: UpdateVideoTranscoderReply => handleUpdateVideoTranscoderReply(msg)
case msg: DestroyVideoTranscoderReply => handleDestroyVideoTranscoderReply(msg)
case msg: TranscodingFinishedUnsuccessfully => handleTranscodingFinishedUnsuccessfully(msg)
case msg: TranscodingFinishedSuccessfully => handleTranscodingFinishedSuccessfully(msg)
case msg: RestartVideoTranscoderReply => handleRestartVideoTranscoderReply(msg)
case msg: StartVideoProbingReply => handleStartVideoProbingReply(msg)
case _ => // do nothing
}
private def handleStartTranscoderRequest(msg: StartTranscoderRequest) {
log.info("\n > Received StartTranscoderRequest. Params:\n"
+ " meetingId = " + msg.meetingId + "\n"
+ " transcoderId = " + msg.transcoderId + "\n"
+ " params = " + msg.params.toString() + "\n")
transcodersModel.getTranscoder(msg.meetingId, msg.transcoderId) match {
case Some(vt) => {
log.info("\n > Found a transcoder for this user {}", msg.transcoderId)
vt ! new StartVideoTranscoderRequest()
}
case None => {
val vt = context.actorOf(VideoTranscoder.props(self, msg.meetingId, msg.transcoderId, msg.params))
transcodersModel.addTranscoder(msg.meetingId, msg.transcoderId, vt)
vt ! new StartVideoTranscoderRequest()
}
}
}
private def handleUpdateTranscoderRequest(msg: UpdateTranscoderRequest) {
log.info("\n > Received UpdateTranscoderRequest. Params:\n"
+ " meetingId = " + msg.meetingId + "\n"
+ " transcoderId = " + msg.transcoderId + "\n"
+ " params = " + msg.params.toString() + "\n")
transcodersModel.getTranscoder(msg.meetingId, msg.transcoderId) match {
case Some(vt) => vt ! new UpdateVideoTranscoderRequest(msg.params)
case None =>
log.info("\n > Video transcoder with id = {} not found (might be finished already or it is restarting).", msg.transcoderId)
}
}
private def handleStopTranscoderRequest(msg: StopTranscoderRequest) {
log.info("\n > Received StopTranscoderRequest. Params:\n"
+ " meetingId = " + msg.meetingId + "\n"
+ " transcoderId = " + msg.transcoderId + "\n")
transcodersModel.getTranscoder(msg.meetingId, msg.transcoderId) match {
case Some(vt) => {
transcodersModel.removeTranscoder(msg.meetingId, msg.transcoderId)
vt ! new DestroyVideoTranscoderRequest() //stop transcoder and destroy it's actor
}
case None => {
log.info("\n > Transcoder with id = {} not found (might be finished already).", msg.transcoderId)
}
}
}
private def handleStopMeetingTranscoders(msg: StopMeetingTranscoders) {
log.info("\n > Received StopMeetingTranscoders. Params:\n"
+ " meetingId = " + msg.meetingId + "\n")
transcodersModel.getTranscoders(msg.meetingId) foreach {
vt => vt ! new DestroyVideoTranscoderRequest()
}
}
private def handleStartProbingRequest(msg: StartProbingRequest) {
log.info("\n > Received StartProbingRequest. Params:\n"
+ " meetingId = " + msg.meetingId + "\n"
+ " transcoderId = " + msg.transcoderId + "\n")
transcodersModel.getTranscoder(msg.meetingId, msg.transcoderId) match {
case Some(vt) => {
log.info("\n > Found a transcoder for this user {}", msg.transcoderId)
vt ! new StartVideoProbingRequest()
}
case None => {
val vt = context.actorOf(VideoTranscoder.props(self, msg.meetingId, msg.transcoderId, msg.params))
transcodersModel.addTranscoder(msg.meetingId, msg.transcoderId, vt)
vt ! new StartVideoProbingRequest()
}
}
}
}

View File

@ -0,0 +1,78 @@
package org.bigbluebutton.transcode.core.apps
import akka.actor.ActorRef
import org.bigbluebutton.transcode.core.TranscodingActor
import org.bigbluebutton.transcode.api._
import org.bigbluebutton.common.messages.Constants
import org.bigbluebutton.transcode.core.ffmpeg.FFmpegConstants
import scala.collection.JavaConversions._
trait TranscodingObserverApp {
this: TranscodingActor =>
val messageSenderActor: ActorRef
def handleTranscodingFinishedUnsuccessfully(msg: TranscodingFinishedUnsuccessfully) = {
transcodersModel.getTranscoder(msg.getMeetingId(), msg.getTranscoderId()) match {
case Some(vt) => {
log.info("\n > Transcoder for this user {} stopped unsuccessfully, restarting it...", msg.getTranscoderId())
vt ! new RestartVideoTranscoderRequest()
}
case None => {
log.info("\n > Video transcoder with id = {} not found (might be destroyed already).", msg.getTranscoderId())
}
}
}
def handleTranscodingFinishedSuccessfully(msg: TranscodingFinishedSuccessfully) = {
transcodersModel.getTranscoder(msg.getMeetingId(), msg.getTranscoderId()) match {
case Some(vt) => {
log.info("\n > Transcoder for this user {} stopped with success, removing it from transcoder's list...", msg.getTranscoderId())
transcodersModel.removeTranscoder(msg.getMeetingId(), msg.getTranscoderId())
}
case None => {
log.info("\n > Video transcoder with id = {} not found (might be destroyed already).", msg.getTranscoderId())
}
}
}
def handleStartVideoTranscoderReply(msg: StartVideoTranscoderReply) = {
log.info("\n > Transcoder with id = {} started", msg.getTranscoderId())
val params = new scala.collection.mutable.HashMap[String, String]
params += Constants.OUTPUT -> msg.getOutput()
messageSenderActor ! new StartTranscoderReply(msg.getMeetingId(), msg.getTranscoderId(), params)
}
def handleUpdateVideoTranscoderReply(msg: UpdateVideoTranscoderReply) = {
log.info("\n > Transcoder with id = {} updated", msg.getTranscoderId())
val params = new scala.collection.mutable.HashMap[String, String]
params += Constants.OUTPUT -> msg.getOutput()
messageSenderActor ! new UpdateTranscoderReply(msg.getMeetingId(), msg.getTranscoderId(), params)
}
def handleDestroyVideoTranscoderReply(msg: DestroyVideoTranscoderReply) = {
log.info("\n > Transcoder with id = {} stopped", msg.getTranscoderId())
messageSenderActor ! new StopTranscoderReply(msg.getMeetingId(), msg.getTranscoderId())
}
def handleRestartVideoTranscoderReply(msg: RestartVideoTranscoderReply) = {
log.info("\n > Transcoder with id = {} restarted", msg.getTranscoderId())
val params = new scala.collection.mutable.HashMap[String, String]
params += Constants.OUTPUT -> msg.getOutput()
messageSenderActor ! new TranscoderStatusUpdate(msg.getMeetingId(), msg.getTranscoderId(), params)
}
def handleStartVideoProbingReply(msg: StartVideoProbingReply) = {
val ffprobeResult = mapAsScalaMap(msg.getProbingData())
Option(ffprobeResult) match {
case Some(result) =>
val params = new scala.collection.mutable.HashMap[String, String]
params += Constants.WIDTH_RATIO -> result.getOrElse(FFmpegConstants.WIDTH, "")
params += Constants.HEIGHT_RATIO -> result.getOrElse(FFmpegConstants.HEIGHT, "")
messageSenderActor ! new StartProbingReply(msg.getMeetingId(), msg.getTranscoderId(), params)
case _ => log.debug("Could not send ffprobe reply : failed to get the new resolution");
}
}
}

View File

@ -140,4 +140,22 @@ public class Constants {
public static final String GUEST_POLICY = "guest_policy";
public static final String SET_BY = "set_by";
public static final String METADATA = "metadata";
public static final String LOCAL_IP_ADDRESS = "local_ip_address";
public static final String LOCAL_VIDEO_PORT = "local_video_port";
public static final String REMOTE_VIDEO_PORT = "remote_video_port";
public static final String DESTINATION_IP_ADDRESS = "destination_ip_address";
public static final String SIP_HOST = "sip_host";
public static final String TRANSCODER_TYPE = "transcoder_type";
public static final String INPUT = "input";
public static final String OUTPUT = "output";
public static final String TRANSCODE_RTP_TO_RTMP = "transcode_rtp_to_rtmp";
public static final String TRANSCODE_RTMP_TO_RTP = "transcode_rtmp_to_rtp";
public static final String TRANSCODE_FILE_TO_RTP = "transcode_file_to_rtp";
public static final String TRANSCODE_FILE_TO_RTMP = "transcode_file_to_rtmp";
public static final String TRANSCODE_H264_TO_H263 = "transcode_h264_to_h263";
public static final String TRANSCODE_ROTATE_RIGHT = "transcode_rotate_right";
public static final String TRANSCODE_ROTATE_LEFT = "transcode_rotate_left";
public static final String TRANSCODE_ROTATE_UPSIDE_DOWN = "transcode_rotate_upside_down";
public static final String MODULE = "module";
public static final String PROBE_RTMP = "probe_rtmp";
}

View File

@ -52,6 +52,14 @@ public class MessagingConstants {
public static final String FROM_VOICE_CONF_SYSTEM_CHAN = FROM_VOICE_CONF_CHANNEL + ":system";
public static final String FROM_BBB_RECORDING_CHANNEL = "bigbluebutton:from-rap";
public static final String TO_BBB_TRANSCODE_CHANNEL = "bigbluebutton:to-bbb-transcode";
public static final String TO_BBB_TRANSCODE_PATTERN = TO_BBB_TRANSCODE_CHANNEL + ":*";
public static final String TO_BBB_TRANSCODE_SYSTEM_CHAN = TO_BBB_TRANSCODE_CHANNEL + ":system";
public static final String FROM_BBB_TRANSCODE_CHANNEL = "bigbluebutton:from-bbb-transcode";
public static final String FROM_BBB_TRANSCODE_PATTERN = FROM_BBB_TRANSCODE_CHANNEL + ":*";
public static final String FROM_BBB_TRANSCODE_SYSTEM_CHAN = FROM_BBB_TRANSCODE_CHANNEL + ":system";
public static final String DESTROY_MEETING_REQUEST_EVENT = "DestroyMeetingRequestEvent";
public static final String CREATE_MEETING_REQUEST_EVENT = "CreateMeetingRequestEvent";

View File

@ -0,0 +1,64 @@
package org.bigbluebutton.common.messages;
import java.util.Map;
import java.util.HashMap;
import com.google.gson.Gson;
import com.google.gson.JsonObject;
import com.google.gson.JsonParser;
import com.google.gson.reflect.TypeToken;
public class StartProbingReplyMessage implements IBigBlueButtonMessage {
public static final String START_PROBING_REPLY = "start_probing_reply_message";
public static final String VERSION = "0.0.1";
public static final String MEETING_ID = "meeting_id";
public static final String TRANSCODER_ID = "transcoder_id";
public static final String PARAMS = "params";
public final String meetingId;
public final String transcoderId;
public final Map<String,String> params;
public StartProbingReplyMessage(String meetingId, String transcoderId, Map<String,String> params) {
this.meetingId = meetingId;
this.transcoderId = transcoderId;
this.params = params;
}
public String toJson() {
HashMap<String, Object> payload = new HashMap<String, Object>();
payload.put(MEETING_ID, meetingId);
payload.put(TRANSCODER_ID, transcoderId);
payload.put(PARAMS, params);
java.util.HashMap<String, Object> header = MessageBuilder.buildHeader(START_PROBING_REPLY, VERSION, null);
return MessageBuilder.buildJson(header, payload);
}
public static StartProbingReplyMessage fromJson(String message) {
JsonParser parser = new JsonParser();
JsonObject obj = (JsonObject) parser.parse(message);
if (obj.has("header") && obj.has("payload")) {
JsonObject header = (JsonObject) obj.get("header");
JsonObject payload = (JsonObject) obj.get("payload");
if (header.has("name")) {
String messageName = header.get("name").getAsString();
if (START_PROBING_REPLY.equals(messageName)) {
if (payload.has(MEETING_ID)
&& payload.has(TRANSCODER_ID)
&& payload.has(PARAMS)){
String meetingId = payload.get(MEETING_ID).getAsString();
String transcoderId = payload.get(TRANSCODER_ID).getAsString();
Map<String,String> params = new Gson().fromJson(payload.get(PARAMS).toString(), new TypeToken<Map<String, String>>() {}.getType());
return new StartProbingReplyMessage(meetingId, transcoderId, params);
}
}
}
}
return null;
}
}

View File

@ -0,0 +1,64 @@
package org.bigbluebutton.common.messages;
import java.util.Map;
import java.util.HashMap;
import com.google.gson.Gson;
import com.google.gson.JsonObject;
import com.google.gson.JsonParser;
import com.google.gson.reflect.TypeToken;
public class StartProbingRequestMessage implements IBigBlueButtonMessage {
public static final String START_PROBING_REQUEST = "start_probing_request_message";
public static final String VERSION = "0.0.1";
public static final String MEETING_ID = "meeting_id";
public static final String TRANSCODER_ID = "transcoder_id";
public static final String PARAMS = "params";
public final String meetingId;
public final String transcoderId;
public final Map<String,String> params;
public StartProbingRequestMessage(String meetingId, String transcoderId, Map<String,String> params) {
this.meetingId = meetingId;
this.transcoderId = transcoderId;
this.params = params;
}
public String toJson() {
HashMap<String, Object> payload = new HashMap<String, Object>();
payload.put(MEETING_ID, meetingId);
payload.put(TRANSCODER_ID, transcoderId);
payload.put(PARAMS, params);
java.util.HashMap<String, Object> header = MessageBuilder.buildHeader(START_PROBING_REQUEST, VERSION, null);
return MessageBuilder.buildJson(header, payload);
}
public static StartProbingRequestMessage fromJson(String message) {
JsonParser parser = new JsonParser();
JsonObject obj = (JsonObject) parser.parse(message);
if (obj.has("header") && obj.has("payload")) {
JsonObject header = (JsonObject) obj.get("header");
JsonObject payload = (JsonObject) obj.get("payload");
if (header.has("name")) {
String messageName = header.get("name").getAsString();
if (START_PROBING_REQUEST.equals(messageName)) {
if (payload.has(MEETING_ID)
&& payload.has(TRANSCODER_ID)
&& payload.has(PARAMS)){
String meetingId = payload.get(MEETING_ID).getAsString();
String transcoderId = payload.get(TRANSCODER_ID).getAsString();
Map<String,String> params = new Gson().fromJson(payload.get(PARAMS).toString(), new TypeToken<Map<String, String>>() {}.getType());
return new StartProbingRequestMessage(meetingId, transcoderId, params);
}
}
}
}
return null;
}
}

View File

@ -0,0 +1,64 @@
package org.bigbluebutton.common.messages;
import java.util.Map;
import java.util.HashMap;
import com.google.gson.Gson;
import com.google.gson.JsonObject;
import com.google.gson.JsonParser;
import com.google.gson.reflect.TypeToken;
public class StartTranscoderReplyMessage implements IBigBlueButtonMessage {
public static final String START_TRANSCODER_REPLY = "start_transcoder_reply_message";
public static final String VERSION = "0.0.1";
public static final String MEETING_ID = "meeting_id";
public static final String TRANSCODER_ID = "transcoder_id";
public static final String PARAMS = "params";
public final String meetingId;
public final String transcoderId;
public final Map<String,String> params;
public StartTranscoderReplyMessage(String meetingId, String transcoderId, Map<String,String> params) {
this.meetingId = meetingId;
this.transcoderId = transcoderId;
this.params = params;
}
public String toJson() {
HashMap<String, Object> payload = new HashMap<String, Object>();
payload.put(MEETING_ID, meetingId);
payload.put(TRANSCODER_ID, transcoderId);
payload.put(PARAMS, params);
java.util.HashMap<String, Object> header = MessageBuilder.buildHeader(START_TRANSCODER_REPLY, VERSION, null);
return MessageBuilder.buildJson(header, payload);
}
public static StartTranscoderReplyMessage fromJson(String message) {
JsonParser parser = new JsonParser();
JsonObject obj = (JsonObject) parser.parse(message);
if (obj.has("header") && obj.has("payload")) {
JsonObject header = (JsonObject) obj.get("header");
JsonObject payload = (JsonObject) obj.get("payload");
if (header.has("name")) {
String messageName = header.get("name").getAsString();
if (START_TRANSCODER_REPLY.equals(messageName)) {
if ( payload.has(MEETING_ID)
&& payload.has(TRANSCODER_ID)
&& payload.has(PARAMS)){
String meetingId = payload.get(MEETING_ID).getAsString();
String transcoderId = payload.get(TRANSCODER_ID).getAsString();
Map<String,String> params = new Gson().fromJson(payload.get(PARAMS).toString(), new TypeToken<Map<String, String>>() {}.getType());
return new StartTranscoderReplyMessage(meetingId, transcoderId, params);
}
}
}
}
return null;
}
}

View File

@ -0,0 +1,64 @@
package org.bigbluebutton.common.messages;
import java.util.Map;
import java.util.HashMap;
import com.google.gson.Gson;
import com.google.gson.JsonObject;
import com.google.gson.JsonParser;
import com.google.gson.reflect.TypeToken;
public class StartTranscoderRequestMessage implements IBigBlueButtonMessage {
public static final String START_TRANSCODER_REQUEST = "start_transcoder_request_message";
public static final String VERSION = "0.0.1";
public static final String MEETING_ID = "meeting_id";
public static final String TRANSCODER_ID = "transcoder_id";
public static final String PARAMS = "params";
public final String meetingId;
public final String transcoderId;
public final Map<String,String> params;
public StartTranscoderRequestMessage(String meetingId, String transcoderId, Map<String,String> params) {
this.meetingId = meetingId;
this.transcoderId = transcoderId;
this.params = params;
}
public String toJson() {
HashMap<String, Object> payload = new HashMap<String, Object>();
payload.put(MEETING_ID, meetingId);
payload.put(TRANSCODER_ID, transcoderId);
payload.put(PARAMS, params);
java.util.HashMap<String, Object> header = MessageBuilder.buildHeader(START_TRANSCODER_REQUEST, VERSION, null);
return MessageBuilder.buildJson(header, payload);
}
public static StartTranscoderRequestMessage fromJson(String message) {
JsonParser parser = new JsonParser();
JsonObject obj = (JsonObject) parser.parse(message);
if (obj.has("header") && obj.has("payload")) {
JsonObject header = (JsonObject) obj.get("header");
JsonObject payload = (JsonObject) obj.get("payload");
if (header.has("name")) {
String messageName = header.get("name").getAsString();
if (START_TRANSCODER_REQUEST.equals(messageName)) {
if (payload.has(MEETING_ID)
&& payload.has(TRANSCODER_ID)
&& payload.has(PARAMS)){
String meetingId = payload.get(MEETING_ID).getAsString();
String transcoderId = payload.get(TRANSCODER_ID).getAsString();
Map<String,String> params = new Gson().fromJson(payload.get(PARAMS).toString(), new TypeToken<Map<String, String>>() {}.getType());
return new StartTranscoderRequestMessage(meetingId, transcoderId, params);
}
}
}
}
return null;
}
}

View File

@ -0,0 +1,52 @@
package org.bigbluebutton.common.messages;
import java.util.Map;
import java.util.HashMap;
import com.google.gson.Gson;
import com.google.gson.JsonObject;
import com.google.gson.JsonParser;
import com.google.gson.reflect.TypeToken;
public class StopMeetingTranscodersMessage implements IBigBlueButtonMessage {
public static final String STOP_MEETING_TRANSCODERS = "stop_meeting_transcoders_message";
public static final String VERSION = "0.0.1";
public static final String MEETING_ID = "meeting_id";
public final String meetingId;
public StopMeetingTranscodersMessage(String meetingId) {
this.meetingId = meetingId;
}
public String toJson() {
HashMap<String, Object> payload = new HashMap<String, Object>();
payload.put(MEETING_ID, meetingId);
java.util.HashMap<String, Object> header = MessageBuilder.buildHeader(STOP_MEETING_TRANSCODERS, VERSION, null);
return MessageBuilder.buildJson(header, payload);
}
public static StopMeetingTranscodersMessage fromJson(String message) {
JsonParser parser = new JsonParser();
JsonObject obj = (JsonObject) parser.parse(message);
if (obj.has("header") && obj.has("payload")) {
JsonObject header = (JsonObject) obj.get("header");
JsonObject payload = (JsonObject) obj.get("payload");
if (header.has("name")) {
String messageName = header.get("name").getAsString();
if (STOP_MEETING_TRANSCODERS.equals(messageName)) {
if (payload.has(MEETING_ID)){
String meetingId = payload.get(MEETING_ID).getAsString();
return new StopMeetingTranscodersMessage(meetingId);
}
}
}
}
return null;
}
}

View File

@ -0,0 +1,58 @@
package org.bigbluebutton.common.messages;
import java.util.Map;
import java.util.HashMap;
import com.google.gson.Gson;
import com.google.gson.JsonObject;
import com.google.gson.JsonParser;
import com.google.gson.reflect.TypeToken;
public class StopTranscoderReplyMessage implements IBigBlueButtonMessage {
public static final String STOP_TRANSCODER_REPLY = "stop_transcoder_reply_message";
public static final String VERSION = "0.0.1";
public static final String MEETING_ID = "meeting_id";
public static final String TRANSCODER_ID = "transcoder_id";
public final String meetingId;
public final String transcoderId;
public StopTranscoderReplyMessage(String meetingId, String transcoderId) {
this.meetingId = meetingId;
this.transcoderId = transcoderId;
}
public String toJson() {
HashMap<String, Object> payload = new HashMap<String, Object>();
payload.put(MEETING_ID, meetingId);
payload.put(TRANSCODER_ID, transcoderId);
java.util.HashMap<String, Object> header = MessageBuilder.buildHeader(STOP_TRANSCODER_REPLY, VERSION, null);
return MessageBuilder.buildJson(header, payload);
}
public static StopTranscoderReplyMessage fromJson(String message) {
JsonParser parser = new JsonParser();
JsonObject obj = (JsonObject) parser.parse(message);
if (obj.has("header") && obj.has("payload")) {
JsonObject header = (JsonObject) obj.get("header");
JsonObject payload = (JsonObject) obj.get("payload");
if (header.has("name")) {
String messageName = header.get("name").getAsString();
if (STOP_TRANSCODER_REPLY.equals(messageName)) {
if ( payload.has(MEETING_ID)
&& payload.has(TRANSCODER_ID)){
String meetingId = payload.get(MEETING_ID).getAsString();
String transcoderId = payload.get(TRANSCODER_ID).getAsString();
return new StopTranscoderReplyMessage(meetingId, transcoderId);
}
}
}
}
return null;
}
}

View File

@ -0,0 +1,58 @@
package org.bigbluebutton.common.messages;
import java.util.Map;
import java.util.HashMap;
import com.google.gson.Gson;
import com.google.gson.JsonObject;
import com.google.gson.JsonParser;
import com.google.gson.reflect.TypeToken;
public class StopTranscoderRequestMessage implements IBigBlueButtonMessage {
public static final String STOP_TRANSCODER_REQUEST = "stop_transcoder_request_message";
public static final String VERSION = "0.0.1";
public static final String MEETING_ID = "meeting_id";
public static final String TRANSCODER_ID = "transcoder_id";
public final String meetingId;
public final String transcoderId;
public StopTranscoderRequestMessage(String meetingId, String transcoderId) {
this.meetingId = meetingId;
this.transcoderId = transcoderId;
}
public String toJson() {
HashMap<String, Object> payload = new HashMap<String, Object>();
payload.put(MEETING_ID, meetingId);
payload.put(TRANSCODER_ID, transcoderId);
java.util.HashMap<String, Object> header = MessageBuilder.buildHeader(STOP_TRANSCODER_REQUEST, VERSION, null);
return MessageBuilder.buildJson(header, payload);
}
public static StopTranscoderRequestMessage fromJson(String message) {
JsonParser parser = new JsonParser();
JsonObject obj = (JsonObject) parser.parse(message);
if (obj.has("header") && obj.has("payload")) {
JsonObject header = (JsonObject) obj.get("header");
JsonObject payload = (JsonObject) obj.get("payload");
if (header.has("name")) {
String messageName = header.get("name").getAsString();
if (STOP_TRANSCODER_REQUEST.equals(messageName)) {
if (payload.has(MEETING_ID)
&& payload.has(TRANSCODER_ID)){
String meetingId = payload.get(MEETING_ID).getAsString();
String transcoderId = payload.get(TRANSCODER_ID).getAsString();
return new StopTranscoderRequestMessage(meetingId, transcoderId);
}
}
}
}
return null;
}
}

View File

@ -0,0 +1,64 @@
package org.bigbluebutton.common.messages;
import java.util.Map;
import java.util.HashMap;
import com.google.gson.Gson;
import com.google.gson.JsonObject;
import com.google.gson.JsonParser;
import com.google.gson.reflect.TypeToken;
public class TranscoderStatusUpdateMessage implements IBigBlueButtonMessage {
public static final String TRANSCODER_STATUS_UPDATE = "transcoder_status_update";
public static final String VERSION = "0.0.1";
public static final String MEETING_ID = "meeting_id";
public static final String TRANSCODER_ID = "transcoder_id";
public static final String PARAMS = "params";
public final String meetingId;
public final String transcoderId;
public final Map<String,String> params;
public TranscoderStatusUpdateMessage(String meetingId, String transcoderId, Map<String,String> params) {
this.meetingId = meetingId;
this.transcoderId = transcoderId;
this.params = params;
}
public String toJson() {
HashMap<String, Object> payload = new HashMap<String, Object>();
payload.put(MEETING_ID, meetingId);
payload.put(TRANSCODER_ID, transcoderId);
payload.put(PARAMS, params);
java.util.HashMap<String, Object> header = MessageBuilder.buildHeader(TRANSCODER_STATUS_UPDATE, VERSION, null);
return MessageBuilder.buildJson(header, payload);
}
public static TranscoderStatusUpdateMessage fromJson(String message) {
JsonParser parser = new JsonParser();
JsonObject obj = (JsonObject) parser.parse(message);
if (obj.has("header") && obj.has("payload")) {
JsonObject header = (JsonObject) obj.get("header");
JsonObject payload = (JsonObject) obj.get("payload");
if (header.has("name")) {
String messageName = header.get("name").getAsString();
if (TRANSCODER_STATUS_UPDATE.equals(messageName)) {
if (payload.has(MEETING_ID)
&& payload.has(TRANSCODER_ID)
&& payload.has(PARAMS)){
String meetingId = payload.get(MEETING_ID).getAsString();
String transcoderId = payload.get(TRANSCODER_ID).getAsString();
Map<String,String> params = new Gson().fromJson(payload.get(PARAMS).toString(), new TypeToken<Map<String, String>>() {}.getType());
return new TranscoderStatusUpdateMessage(meetingId, transcoderId, params);
}
}
}
}
return null;
}
}

View File

@ -0,0 +1,64 @@
package org.bigbluebutton.common.messages;
import java.util.Map;
import java.util.HashMap;
import com.google.gson.Gson;
import com.google.gson.JsonObject;
import com.google.gson.JsonParser;
import com.google.gson.reflect.TypeToken;
public class UpdateTranscoderReplyMessage implements IBigBlueButtonMessage {
public static final String UPDATE_TRANSCODER_REPLY = "update_transcoder_reply_message";
public static final String VERSION = "0.0.1";
public static final String MEETING_ID = "meeting_id";
public static final String TRANSCODER_ID = "transcoder_id";
public static final String PARAMS = "params";
public final String meetingId;
public final String transcoderId;
public final Map<String,String> params;
public UpdateTranscoderReplyMessage(String meetingId, String transcoderId, Map<String,String> params) {
this.meetingId = meetingId;
this.transcoderId = transcoderId;
this.params = params;
}
public String toJson() {
HashMap<String, Object> payload = new HashMap<String, Object>();
payload.put(MEETING_ID, meetingId);
payload.put(TRANSCODER_ID, transcoderId);
payload.put(PARAMS, params);
java.util.HashMap<String, Object> header = MessageBuilder.buildHeader(UPDATE_TRANSCODER_REPLY, VERSION, null);
return MessageBuilder.buildJson(header, payload);
}
public static UpdateTranscoderReplyMessage fromJson(String message) {
JsonParser parser = new JsonParser();
JsonObject obj = (JsonObject) parser.parse(message);
if (obj.has("header") && obj.has("payload")) {
JsonObject header = (JsonObject) obj.get("header");
JsonObject payload = (JsonObject) obj.get("payload");
if (header.has("name")) {
String messageName = header.get("name").getAsString();
if (UPDATE_TRANSCODER_REPLY.equals(messageName)) {
if ( payload.has(MEETING_ID)
&& payload.has(TRANSCODER_ID)
&& payload.has(PARAMS)){
String meetingId = payload.get(MEETING_ID).getAsString();
String transcoderId = payload.get(TRANSCODER_ID).getAsString();
Map<String,String> params = new Gson().fromJson(payload.get(PARAMS).toString(), new TypeToken<Map<String, String>>() {}.getType());
return new UpdateTranscoderReplyMessage(meetingId, transcoderId, params);
}
}
}
}
return null;
}
}

View File

@ -0,0 +1,64 @@
package org.bigbluebutton.common.messages;
import java.util.Map;
import java.util.HashMap;
import com.google.gson.Gson;
import com.google.gson.JsonObject;
import com.google.gson.JsonParser;
import com.google.gson.reflect.TypeToken;
public class UpdateTranscoderRequestMessage implements IBigBlueButtonMessage {
public static final String UPDATE_TRANSCODER_REQUEST = "update_transcoder_request_message";
public static final String VERSION = "0.0.1";
public static final String MEETING_ID = "meeting_id";
public static final String TRANSCODER_ID = "transcoder_id";
public static final String PARAMS = "params";
public final String meetingId;
public final String transcoderId;
public final Map<String,String> params;
public UpdateTranscoderRequestMessage(String meetingId, String transcoderId, Map<String,String> params) {
this.meetingId = meetingId;
this.transcoderId = transcoderId;
this.params = params;
}
public String toJson() {
HashMap<String, Object> payload = new HashMap<String, Object>();
payload.put(MEETING_ID, meetingId);
payload.put(TRANSCODER_ID, transcoderId);
payload.put(PARAMS, params);
java.util.HashMap<String, Object> header = MessageBuilder.buildHeader(UPDATE_TRANSCODER_REQUEST, VERSION, null);
return MessageBuilder.buildJson(header, payload);
}
public static UpdateTranscoderRequestMessage fromJson(String message) {
JsonParser parser = new JsonParser();
JsonObject obj = (JsonObject) parser.parse(message);
if (obj.has("header") && obj.has("payload")) {
JsonObject header = (JsonObject) obj.get("header");
JsonObject payload = (JsonObject) obj.get("payload");
if (header.has("name")) {
String messageName = header.get("name").getAsString();
if (UPDATE_TRANSCODER_REQUEST.equals(messageName)) {
if (payload.has(MEETING_ID)
&& payload.has(TRANSCODER_ID)
&& payload.has(PARAMS)){
String meetingId = payload.get(MEETING_ID).getAsString();
String transcoderId = payload.get(TRANSCODER_ID).getAsString();
Map<String,String> params = new Gson().fromJson(payload.get(PARAMS).toString(), new TypeToken<Map<String, String>>() {}.getType());
return new UpdateTranscoderRequestMessage(meetingId, transcoderId, params);
}
}
}
}
return null;
}
}

View File

@ -202,6 +202,36 @@ public class VideoApplication extends MultiThreadedApplicationAdapter {
log.info("streamPublishStart " + stream.getPublishedName() + " " + System.currentTimeMillis() + " " + conn.getScope().getName());
}
private String getStreamName(String streamName) {
String parts[] = streamName.split("/");
if(parts.length > 1)
return parts[parts.length-1];
return "";
}
private void requestRotateVideoTranscoder(IBroadcastStream stream) {
IConnection conn = Red5.getConnectionLocal();
String userId = getUserId();
String meetingId = conn.getScope().getName();
String streamId = stream.getPublishedName();
String streamName = getStreamName(streamId);
String ipAddress = conn.getHost();
switch (VideoRotator.getDirection(streamId)) {
case VideoRotator.ROTATE_RIGHT:
publisher.startRotateRightTranscoderRequest(meetingId, userId, streamName, ipAddress);
break;
case VideoRotator.ROTATE_LEFT:
publisher.startRotateLeftTranscoderRequest(meetingId, userId, streamName, ipAddress);
break;
case VideoRotator.ROTATE_UPSIDE_DOWN:
publisher.startRotateUpsideDownTranscoderRequest(meetingId, userId, streamName, ipAddress);
break;
default:
break;
}
}
@Override
public void streamBroadcastStart(IBroadcastStream stream) {
IConnection conn = Red5.getConnectionLocal();
@ -214,7 +244,14 @@ public class VideoApplication extends MultiThreadedApplicationAdapter {
Matcher matcher = RECORD_STREAM_ID_PATTERN.matcher(stream.getPublishedName());
if (matcher.matches()) {
addH263PublishedStream(streamId);
if (streamId.contains("/")) {
if(VideoRotator.getDirection(streamId) != null) {
//VideoRotator rotator = new VideoRotator(streamId);
videoRotators.put(streamId, null);
requestRotateVideoTranscoder(stream);
}
} else if (matcher.matches()) {
log.info("Start recording of stream=[" + stream.getPublishedName() + "] for meeting=[" + conn.getScope().getName() + "]");
Boolean recordVideoStream = true;
@ -223,15 +260,7 @@ public class VideoApplication extends MultiThreadedApplicationAdapter {
stream.addStreamListener(listener);
streamListeners.put(conn.getScope().getName() + "-" + stream.getPublishedName(), listener);
addH263PublishedStream(streamId);
if (streamId.contains("/")) {
if(VideoRotator.getDirection(streamId) != null) {
VideoRotator rotator = new VideoRotator(streamId);
videoRotators.put(streamId, rotator);
}
} else {
recordStream(stream);
}
recordStream(stream);
}
@ -265,6 +294,13 @@ public class VideoApplication extends MultiThreadedApplicationAdapter {
String streamId = stream.getPublishedName();
Matcher matcher = RECORD_STREAM_ID_PATTERN.matcher(stream.getPublishedName());
removeH263ConverterIfNeeded(streamId);
if (videoRotators.containsKey(streamId)) {
// Stop rotator
videoRotators.remove(streamId);
publisher.stopTranscoderRequest(meetingId, userId);
}
removeH263PublishedStream(streamId);
if (matcher.matches()) {
IStreamListener listener = streamListeners.remove(scopeName + "-" + stream.getPublishedName());
if (listener != null) {
@ -285,12 +321,6 @@ public class VideoApplication extends MultiThreadedApplicationAdapter {
}
removeH263ConverterIfNeeded(streamId);
if (videoRotators.containsKey(streamId)) {
// Stop rotator
videoRotators.remove(streamId).stop();
}
removeH263PublishedStream(streamId);
}
/**
@ -338,7 +368,7 @@ public class VideoApplication extends MultiThreadedApplicationAdapter {
// Check if a new stream converter is necessary
H263Converter converter;
if(!h263Converters.containsKey(streamName) && !isStreamPublished(streamName)) {
converter = new H263Converter(streamName);
converter = new H263Converter(streamName, publisher);
h263Converters.put(streamName, converter);
}
else {

View File

@ -1,8 +1,6 @@
package org.bigbluebutton.app.video.converter;
import org.bigbluebutton.app.video.ffmpeg.FFmpegCommand;
import org.bigbluebutton.app.video.ffmpeg.ProcessMonitor;
import org.bigbluebutton.app.video.ffmpeg.ProcessMonitorObserver;
import org.bigbluebutton.red5.pubsub.MessagePublisher;
import org.red5.logging.Red5LoggerFactory;
import org.red5.server.api.IConnection;
import org.red5.server.api.Red5;
@ -16,7 +14,7 @@ import org.slf4j.Logger;
* Converted streams are published in the same scope as the original ones,
* with 'h263/' appended in the beginning.
*/
public class H263Converter implements ProcessMonitorObserver{
public class H263Converter {
private static Logger log = Red5LoggerFactory.getLogger(H263Converter.class, "video");
@ -25,8 +23,11 @@ public class H263Converter implements ProcessMonitorObserver{
private String origin;
private Integer numListeners = 0;
private FFmpegCommand ffmpeg;
private ProcessMonitor processMonitor;
private MessagePublisher publisher;
private Boolean publishing;
private String ipAddress;
private String meetingId;
private String userId;
/**
* Creates a H263Converter from a given streamName. It is assumed
@ -35,38 +36,26 @@ public class H263Converter implements ProcessMonitorObserver{
*
* @param origin streamName of the stream that should be converted
*/
public H263Converter(String origin) {
public H263Converter(String origin, MessagePublisher publisher) {
log.info("Spawn FFMpeg to convert H264 to H263 for stream [{}]", origin);
this.origin = origin;
this.publisher = publisher;
this.publishing = false;
IConnection conn = Red5.getConnectionLocal();
String ip = conn.getHost();
String conf = conn.getScope().getName();
String inputLive = "rtmp://" + ip + "/video/" + conf + "/" + origin + " live=1";
String output = "rtmp://" + ip + "/video/" + conf + "/" + H263PREFIX + origin;
ffmpeg = new FFmpegCommand();
ffmpeg.setFFmpegPath("/usr/local/bin/ffmpeg");
ffmpeg.setInput(inputLive);
ffmpeg.setCodec("flv1"); // Sorensen H263
ffmpeg.setFormat("flv");
ffmpeg.setOutput(output);
ffmpeg.setAudioEnabled(false);
ffmpeg.setLoglevel("quiet");
ffmpeg.setAnalyzeDuration("10000"); // 10ms
this.ipAddress = conn.getHost();
this.meetingId = conn.getScope().getName();
this.userId = getUserId();
}
/**
* Launches the process monitor responsible for FFmpeg.
* Launches the transcoder event responsible for FFmpeg.
*/
private void startConverter() {
if (processMonitor == null){
String[] command = ffmpeg.getFFmpegCommand(true);
processMonitor = new ProcessMonitor(command,"FFMPEG");
processMonitor.setProcessMonitorObserver(this);
processMonitor.start();
}else log.debug("No need to start transcoder, it is already running");
if (!publishing) {
publisher.startH264ToH263TranscoderRequest(meetingId, userId, origin, ipAddress);
publishing = true;
} else log.debug("No need to start transcoder, it is already running");
}
/**
@ -102,28 +91,17 @@ public class H263Converter implements ProcessMonitorObserver{
* listeners to zero.
*/
public synchronized void stopConverter() {
if(processMonitor != null) {
if (publishing) {
this.numListeners = 0;
processMonitor.forceDestroy();
processMonitor = null;
publisher.stopTranscoderRequest(meetingId, userId);
publishing = false;
log.debug("Transcoder force-stopped");
}else log.debug("No need to stop transcoder, it already stopped");
} else log.debug("No need to stop transcoder, it already stopped");
}
private synchronized void clearConverterData(){
if(processMonitor!=null){
log.debug("Clearing process monitor's data.");
this.numListeners = 0;
processMonitor=null;
}
}
@Override
public void handleProcessFinishedUnsuccessfully(String processName, String processOutput){}
@Override
public void handleProcessFinishedWithSuccess(String processName, String processOutput){
log.debug("{} finished successfully [output={}]. ",processName,processOutput);
//clearConverterData();
}
private String getUserId() {
String userid = (String) Red5.getConnectionLocal().getAttribute("USERID");
if ((userid == null) || ("".equals(userid))) userid = "unknown-userid";
return userid;
}
}

View File

@ -18,9 +18,10 @@ public class VideoRotator {
public static final String ROTATE_LEFT = "rotate_left";
public static final String ROTATE_RIGHT = "rotate_right";
public static final String ROTATE_UPSIDE_DOWN = "rotate_left/rotate_left";
private String streamName;
private FFmpegCommand.ROTATE direction;
// private FFmpegCommand.ROTATE direction;
private FFmpegCommand ffmpeg;
private ProcessMonitor processMonitor;
@ -35,9 +36,9 @@ public class VideoRotator {
*/
public VideoRotator(String origin) {
this.streamName = getStreamName(origin);
this.direction = getDirection(origin);
// this.direction = getDirection(origin);
log.debug("Setting up VideoRotator: StreamName={}, Direction={}",this.streamName,this.direction);
// log.debug("Setting up VideoRotator: StreamName={}, Direction={}",this.streamName,this.direction);
IConnection conn = Red5.getConnectionLocal();
String ip = conn.getHost();
String conf = conn.getScope().getName();
@ -51,7 +52,7 @@ public class VideoRotator {
ffmpeg.setFormat("flv");
ffmpeg.setOutput(output);
ffmpeg.setLoglevel("warning");
ffmpeg.setRotation(direction);
// ffmpeg.setRotation(direction);
ffmpeg.setAnalyzeDuration("10000"); // 10ms
start();
@ -72,16 +73,22 @@ public class VideoRotator {
/**
* Get the rotate direction from the streamName string.
* @param streamName Name of the stream with rotate direction
* @return FFmpegCommand.ROTATE for the given direction if present, null otherwise
* @return String for the given direction if present, null otherwise
*/
public static FFmpegCommand.ROTATE getDirection(String streamName) {
String parts[] = streamName.split("/");
public static String getDirection(String streamName) {
int index = streamName.lastIndexOf("/");
String parts[] = {
streamName.substring(0, index),
streamName.substring(index + 1)
};
switch(parts[0]) {
case ROTATE_LEFT:
return FFmpegCommand.ROTATE.LEFT;
return ROTATE_LEFT;
case ROTATE_RIGHT:
return FFmpegCommand.ROTATE.RIGHT;
return ROTATE_RIGHT;
case ROTATE_UPSIDE_DOWN:
return ROTATE_UPSIDE_DOWN;
default:
return null;
}
@ -91,7 +98,7 @@ public class VideoRotator {
* Start FFmpeg process to rotate and re-publish the stream.
*/
public void start() {
log.debug("Spawn FFMpeg to rotate [{}] stream [{}]", direction.name(), streamName);
// log.debug("Spawn FFMpeg to rotate [{}] stream [{}]", direction.name(), streamName);
String[] command = ffmpeg.getFFmpegCommand(true);
if (processMonitor == null) {
processMonitor = new ProcessMonitor(command,"FFMPEG");
@ -103,7 +110,7 @@ public class VideoRotator {
* Stop FFmpeg process that is rotating and re-publishing the stream.
*/
public void stop() {
log.debug("Stopping FFMpeg from rotate [{}] stream [{}]", direction.name(), streamName);
// log.debug("Stopping FFMpeg from rotate [{}] stream [{}]", direction.name(), streamName);
if(processMonitor != null) {
processMonitor.destroy();
processMonitor = null;

View File

@ -12,7 +12,7 @@ public class FFmpegCommand {
/**
* Indicate the direction to rotate the video
*/
public enum ROTATE { LEFT, RIGHT };
public enum ROTATE { LEFT, RIGHT, UPSIDE_DOWN };
private HashMap args;
private HashMap x264Params;
@ -164,6 +164,9 @@ public class FFmpegCommand {
case RIGHT:
this.args.put("-vf", "transpose=1");
break;
case UPSIDE_DOWN:
this.args.put("-vf", "transpose=2,transpose=2");
break;
}
}

View File

@ -1,6 +1,14 @@
package org.bigbluebutton.red5.pubsub;
import org.bigbluebutton.common.messages.*;
import org.bigbluebutton.common.messages.MessagingConstants;
import org.bigbluebutton.common.messages.Constants;
import org.bigbluebutton.common.messages.UserSharedWebcamMessage;
import org.bigbluebutton.common.messages.UserUnshareWebcamRequestMessage;
import org.bigbluebutton.common.messages.StartTranscoderRequestMessage;
import org.bigbluebutton.common.messages.StopTranscoderRequestMessage;
import java.util.HashMap;
import java.util.Map;
public class MessagePublisher {
@ -21,4 +29,54 @@ public class MessagePublisher {
sender.send(MessagingConstants.TO_USERS_CHANNEL, msg.toJson());
}
public void startRotateLeftTranscoderRequest(String meetingId, String userId, String streamName, String ipAddress) {
Map<String, String> params = new HashMap<String, String>();
params.put(Constants.TRANSCODER_TYPE, Constants.TRANSCODE_ROTATE_LEFT);
params.put(Constants.LOCAL_IP_ADDRESS, ipAddress);
params.put(Constants.DESTINATION_IP_ADDRESS, ipAddress);
params.put(Constants.INPUT, streamName);
// TODO: transcoderId is getting userId, this may have to change
StartTranscoderRequestMessage msg = new StartTranscoderRequestMessage(meetingId, userId, params);
sender.send(MessagingConstants.TO_BBB_TRANSCODE_SYSTEM_CHAN, msg.toJson());
}
public void startRotateRightTranscoderRequest(String meetingId, String userId, String streamName, String ipAddress) {
Map<String, String> params = new HashMap<String, String>();
params.put(Constants.TRANSCODER_TYPE, Constants.TRANSCODE_ROTATE_RIGHT);
params.put(Constants.LOCAL_IP_ADDRESS, ipAddress);
params.put(Constants.DESTINATION_IP_ADDRESS, ipAddress);
params.put(Constants.INPUT, streamName);
// TODO: transcoderId is getting userId, this may have to change
StartTranscoderRequestMessage msg = new StartTranscoderRequestMessage(meetingId, userId, params);
sender.send(MessagingConstants.TO_BBB_TRANSCODE_SYSTEM_CHAN, msg.toJson());
}
public void startRotateUpsideDownTranscoderRequest(String meetingId, String userId, String streamName, String ipAddress) {
Map<String, String> params = new HashMap<String, String>();
params.put(Constants.TRANSCODER_TYPE, Constants.TRANSCODE_ROTATE_UPSIDE_DOWN);
params.put(Constants.LOCAL_IP_ADDRESS, ipAddress);
params.put(Constants.DESTINATION_IP_ADDRESS, ipAddress);
params.put(Constants.INPUT, streamName);
// TODO: transcoderId is getting userId, this may have to change
StartTranscoderRequestMessage msg = new StartTranscoderRequestMessage(meetingId, userId, params);
sender.send(MessagingConstants.TO_BBB_TRANSCODE_SYSTEM_CHAN, msg.toJson());
}
public void startH264ToH263TranscoderRequest(String meetingId, String userId, String streamName, String ipAddress) {
Map<String, String> params = new HashMap<String, String>();
params.put(Constants.TRANSCODER_TYPE, Constants.TRANSCODE_H264_TO_H263);
params.put(Constants.MODULE, "video");
params.put(Constants.LOCAL_IP_ADDRESS, ipAddress);
params.put(Constants.DESTINATION_IP_ADDRESS, ipAddress);
params.put(Constants.INPUT, streamName);
// TODO: transcoderId is getting userId, this may have to change
StartTranscoderRequestMessage msg = new StartTranscoderRequestMessage(meetingId, userId, params);
sender.send(MessagingConstants.TO_BBB_TRANSCODE_SYSTEM_CHAN, msg.toJson());
}
public void stopTranscoderRequest(String meetingId, String userId) {
// TODO: transcoderId is getting userId, this may have to change
StopTranscoderRequestMessage msg = new StopTranscoderRequestMessage(meetingId, userId);
sender.send(MessagingConstants.TO_BBB_TRANSCODE_SYSTEM_CHAN, msg.toJson());
}
}

View File

@ -426,6 +426,7 @@ bbb.screenshareView.actualSize = Display actual size
bbb.screenshareView.minimizeBtn.accessibilityName = Minimize the Screen Sharing View Window
bbb.screenshareView.maximizeRestoreBtn.accessibilityName = Maximize the Screen Sharing View Window
bbb.screenshareView.closeBtn.accessibilityName = Close the Screen Sharing View Window
bbb.toolbar.sharednotes.toolTip = Open Shared Notes
bbb.toolbar.phone.toolTip.start = Enable Audio (microphone or listen only)
bbb.toolbar.phone.toolTip.stop = Disable Audio
bbb.toolbar.phone.toolTip.mute = Stop listening the conference

View File

@ -410,6 +410,7 @@ bbb.screenshareView.actualSize = Exibir tamanho original
bbb.screenshareView.minimizeBtn.accessibilityName = Minimizar a Janela de Visualização do Compartilhamento de Tela
bbb.screenshareView.maximizeRestoreBtn.accessibilityName = Maximizar a Janela de Visualização do Compartilhamento de Tela
bbb.screenshareView.closeBtn.accessibilityName = Fechar a Janela de Visualização do Compartilhamento de Tela
bbb.toolbar.sharednotes.toolTip = Abrir bloco de notas
bbb.toolbar.phone.toolTip.start = Transmitir seu microfone
bbb.toolbar.phone.toolTip.stop = Interromper transmissão do seu microfone
bbb.toolbar.phone.toolTip.mute = Parar de escutar a conferência

View File

@ -120,6 +120,11 @@
<script src="lib/verto-min.js" language="javascript"></script>
<script src="lib/verto_extension.js" language="javascript"></script>
<script src="lib/kurento-utils.min.js" language="javascript"></script>
<script src="lib/kurento-extension.js" language="javascript"></script>
<script src="lib/adapter.js?v=VERSION" language="javascript"></script>
<script src="lib/bbb_api_bridge.js?v=VERSION" language="javascript"></script>
<script src="lib/sip.js?v=VERSION" language="javascript"></script>

View File

@ -0,0 +1,433 @@
var isFirefox = typeof window.InstallTrigger !== 'undefined';
var isOpera = !!window.opera || navigator.userAgent.indexOf(' OPR/') >= 0;
var isChrome = !!window.chrome && !isOpera;
var kurentoHandler = null;
Kurento = function (
tag,
voiceBridge,
conferenceUsername,
internalMeetingId,
onFail = null,
chromeExtension = null
) {
this.ws = null;
this.video;
this.screen;
this.webRtcPeer;
this.extensionInstalled = false;
this.screenConstraints = {};
this.mediaCallback = null;
voiceBridge += "-DESKSHARE";
this.vid_width = window.screen.width;
this.vid_height = window.screen.height;
// TODO properly generate a uuid
this.sessid = Math.random().toString();
this.renderTag = 'remote-media';
this.destination_number = internalMeetingId;
this.caller_id_name = conferenceUsername;
this.caller_id_number = conferenceUsername;
this.pingInterval;
this.kurentoPort = "kurento-screenshare";
this.hostName = window.location.hostname;
this.socketUrl = 'wss://' + this.hostName + '/' + this.kurentoPort;
this.iceServers = null;
if (chromeExtension != null) {
this.chromeExtension = chromeExtension;
}
if (onFail != null) {
this.onFail = Kurento.normalizeCallback(onFail);
} else {
var _this = this;
this.onFail = function () {
_this.logError('Default error handler');
};
}
};
this.KurentoManager= function () {
this.kurentoVideo = null;
this.kurentoScreenShare = null;
};
KurentoManager.prototype.exitScreenShare = function () {
if (this.kurentoScreenShare != null) {
if(kurentoHandler.pingInterval) {
clearInterval(kurentoHandler.pingInterval);
}
if(kurentoHandler.ws !== null) {
kurentoHandler.ws.onclose = function(){};
kurentoHandler.ws.close();
}
kurentoHandler.disposeScreenShare();
this.kurentoScreenShare = null;
kurentoHandler = null;
}
};
KurentoManager.prototype.shareScreen = function (tag) {
this.exitScreenShare();
var obj = Object.create(Kurento.prototype);
Kurento.apply(obj, arguments);
this.kurentoScreenShare = obj;
kurentoHandler = obj;
this.kurentoScreenShare.setScreenShare(tag);
};
// Still unused, part of the HTML5 implementation
KurentoManager.prototype.joinWatchVideo = function (tag) {
this.exitVideo();
var obj = Object.create(Kurento.prototype);
Kurento.apply(obj, arguments);
this.kurentoVideo = obj;
kurentoHandler = obj;
this.kurentoVideo.setWatchVideo(tag);
};
Kurento.prototype.setScreenShare = function (tag) {
this.mediaCallback = this.makeShare;
this.create(tag);
};
Kurento.prototype.create = function (tag) {
this.setRenderTag(tag);
this.iceServers = true;
this.init();
};
Kurento.prototype.init = function () {
var self = this;
if("WebSocket" in window) {
console.log("this browser supports websockets");
this.ws = new WebSocket(this.socketUrl);
this.ws.onmessage = this.onWSMessage;
this.ws.onclose = function (close) {
kurentoManager.exitScreenShare();
self.onFail("Websocket connection closed");
};
this.ws.onerror = function (error) {
kurentoManager.exitScreenShare();
self.onFail("Websocket connection error");
};
this.ws.onopen = function() {
self.pingInterval = setInterval(self.ping, 3000);
self.mediaCallback();
};
}
else
console.log("this browser does not support websockets");
};
Kurento.prototype.onWSMessage = function (message) {
var parsedMessage = JSON.parse(message.data);
switch (parsedMessage.id) {
case 'presenterResponse':
kurentoHandler.presenterResponse(parsedMessage);
break;
case 'stopSharing':
kurentoManager.exitScreenShare();
break;
case 'iceCandidate':
kurentoHandler.webRtcPeer.addIceCandidate(parsedMessage.candidate);
break;
case 'pong':
break;
default:
console.error('Unrecognized message', parsedMessage);
}
};
Kurento.prototype.setRenderTag = function (tag) {
this.renderTag = tag;
};
Kurento.prototype.presenterResponse = function (message) {
if (message.response != 'accepted') {
var errorMsg = message.message ? message.message : 'Unknow error';
console.warn('Call not accepted for the following reason: ' + errorMsg);
kurentoManager.exitScreenShare();
kurentoHandler.onFail(errorMessage);
} else {
console.log("Presenter call was accepted with SDP => " + message.sdpAnswer);
this.webRtcPeer.processAnswer(message.sdpAnswer);
}
}
Kurento.prototype.serverResponse = function (message) {
if (message.response != 'accepted') {
var errorMsg = message.message ? message.message : 'Unknow error';
console.warn('Call not accepted for the following reason: ' + errorMsg);
kurentoHandler.dispose();
} else {
this.webRtcPeer.processAnswer(message.sdpAnswer);
}
}
Kurento.prototype.makeShare = function() {
var self = this;
console.log("Kurento.prototype.makeShare " + JSON.stringify(this.webRtcPeer, null, 2));
if (!this.webRtcPeer) {
var options = {
onicecandidate : this.onIceCandidate
}
console.log("Peer options " + JSON.stringify(options, null, 2));
kurentoHandler.startScreenStreamFrom();
}
}
Kurento.prototype.onOfferPresenter = function (error, offerSdp) {
if(error) {
console.log("Kurento.prototype.onOfferPresenter Error " + error);
kurentoHandler.onFail(error);
return;
}
var message = {
id : 'presenter',
presenterId : kurentoHandler.sessid,
callerName : kurentoHandler.caller_id_name,
voiceBridge : kurentoHandler.destination_number,
sdpOffer : offerSdp,
vh: kurentoHandler.vid_height,
vw: kurentoHandler.vid_width
};
console.log("onOfferPresenter sending to screenshare server => " + JSON.stringify(message, null, 2));
kurentoHandler.sendMessage(message);
}
Kurento.prototype.startScreenStreamFrom = function () {
var screenInfo = null;
var _this = this;
if (!!window.chrome) {
if (!_this.chromeExtension) {
_this.logError({
status: 'failed',
message: 'Missing Chrome Extension key',
});
_this.onFail();
return;
}
}
// TODO it would be nice to check those constraints
_this.screenConstraints.video = {};
var options = {
//localVideo: this.renderTag,
onicecandidate : _this.onIceCandidate,
mediaConstraints : _this.screenConstraints,
sendSource : 'desktop'
};
console.log(" Peer options => " + JSON.stringify(options, null, 2));
_this.webRtcPeer = kurentoUtils.WebRtcPeer.WebRtcPeerSendonly(options, function(error) {
if(error) {
console.log("WebRtcPeerSendonly constructor error " + JSON.stringify(error, null, 2));
kurentoHandler.onFail(error);
return kurentoManager.exitScreenShare();
}
_this.webRtcPeer.generateOffer(_this.onOfferPresenter);
console.log("Generated peer offer w/ options " + JSON.stringify(options));
});
}
Kurento.prototype.onIceCandidate = function(candidate) {
console.log('Local candidate' + JSON.stringify(candidate));
var message = {
id : 'onIceCandidate',
presenterId : kurentoHandler.sessid,
candidate : candidate
}
console.log("this object " + JSON.stringify(this, null, 2));
kurentoHandler.sendMessage(message);
}
Kurento.prototype.setWatchVideo = function (tag) {
this.useVideo = true;
this.useCamera = 'none';
this.useMic = 'none';
this.mediaCallback = this.viewer;
this.create(tag);
};
Kurento.prototype.viewer = function () {
var self = this;
if (!this.webRtcPeer) {
var options = {
remoteVideo: this.renderTag,
onicecandidate : onIceCandidate
}
webRtcPeer = kurentoUtils.WebRtcPeer.WebRtcPeerRecvonly(options, function(error) {
if(error) {
return kurentoHandler.onFail(error);
}
this.generateOffer(onOfferViewer);
});
}
};
Kurento.prototype.onOfferViewer = function (error, offerSdp) {
if(error) {
console.log("Kurento.prototype.onOfferViewer Error " + error);
return kurentoHandler.onFail();
}
var message = {
id : 'viewer',
presenterId : kurentoHandler.sessid,
callerName : kurentoHandler.caller_id_name,
voiceBridge : kurentoHandler.destination_number,
sdpOffer : offerSdp
};
console.log("onOfferViewer sending to screenshare server => " + JSON.stringify(message, null, 2));
kurentoHandler.sendMessage(message);
};
Kurento.prototype.ping = function() {
var message = {
id : 'ping',
presenterId : kurentoHandler.sessid,
callerName : kurentoHandler.caller_id_name,
voiceBridge : kurentoHandler.destination_number,
};
kurentoHandler.sendMessage(message);
}
Kurento.prototype.stop = function() {
if (this.webRtcPeer) {
var message = {
id : 'stop',
presenterId : kurentoHandler.sessId,
}
kurentoHandler.sendMessage(message);
kurentoHandler.disposeScreenShare();
}
}
Kurento.prototype.dispose = function() {
if (this.webRtcPeer) {
this.webRtcPeer.dispose();
this.webRtcPeer = null;
}
}
Kurento.prototype.disposeScreenShare = function() {
if (this.webRtcPeer) {
this.webRtcPeer.dispose();
this.webRtcPeer = null;
}
}
Kurento.prototype.sendMessage = function(message) {
var jsonMessage = JSON.stringify(message);
console.log('Sending message: ' + jsonMessage);
this.ws.send(jsonMessage);
}
Kurento.prototype.logger = function (obj) {
console.log(obj);
};
Kurento.prototype.logError = function (obj) {
console.error(obj);
};
Kurento.prototype.getChromeScreenConstraints = function(callback, extensionId) {
chrome.runtime.sendMessage(extensionId, {
getStream: true,
sources: [
"window",
"screen",
"tab"
]},
function(response) {
console.log(response);
callback(response);
});
};
Kurento.normalizeCallback = function (callback) {
if (typeof callback == 'function') {
return callback;
} else {
console.log(document.getElementById('BigBlueButton')[callback]);
return function (args) {
document.getElementById('BigBlueButton')[callback](args);
};
}
};
/* Global methods */
// this function explains how to use above methods/objects
window.getScreenConstraints = function(sendSource, callback) {
var _this = this;
var chromeMediaSourceId = sendSource;
if(isChrome) {
kurentoHandler.getChromeScreenConstraints (function (constraints) {
var sourceId = constraints.streamId;
// this statement sets gets 'sourceId" and sets "chromeMediaSourceId"
kurentoHandler.screenConstraints.video.chromeMediaSource = { exact: [sendSource]};
kurentoHandler.screenConstraints.video.chromeMediaSourceId= sourceId;
console.log("getScreenConstraints for Chrome returns => " +JSON.stringify(kurentoHandler.screenConstraints, null, 2));
// now invoking native getUserMedia API
callback(null, kurentoHandler.screenConstraints);
}, kurentoHandler.chromeExtension);
}
else if (isFirefox) {
kurentoHandler.screenConstraints.video.mediaSource= "screen";
kurentoHandler.screenConstraints.video.width= {max: kurentoHandler.vid_width};
kurentoHandler.screenConstraints.video.height = {max: kurentoHandler.vid_height};
console.log("getScreenConstraints for Firefox returns => " +JSON.stringify(kurentoHandler.screenConstraints, null, 2));
// now invoking native getUserMedia API
callback(null, kurentoHandler.screenConstraints);
}
}
window.kurentoInitialize = function () {
if (window.kurentoManager == null || window.KurentoManager == undefined) {
window.kurentoManager = new KurentoManager();
}
};
window.kurentoShareScreen = function() {
window.kurentoInitialize();
window.kurentoManager.shareScreen.apply(window.kurentoManager, arguments);
};
window.kurentoExitScreenShare = function () {
window.kurentoInitialize();
window.kurentoManager.exitScreenShare();
};
window.kurentoWatchVideo = function () {
window.kurentoInitialize();
window.kurentoManager.joinWatchVideo.apply(window.kurentoManager, arguments);
};

File diff suppressed because one or more lines are too long

View File

@ -119,6 +119,15 @@ package org.bigbluebutton.common
[Embed(source="assets/images/moderator_white.png")]
public var moderator_white:Class;
[Embed(source="assets/images/ic_fullscreen_white.png")]
public var full_screen_white:Class;
[Embed(source="assets/images/ic_fullscreen_exit_white.png")]
public var full_screen_exit_white:Class;
[Embed(source="assets/images/ic_close_white.png")]
public var close_white:Class;
[Embed(source="assets/images/presenter_white.png")]
public var presenter_white:Class;

Binary file not shown.

After

Width:  |  Height:  |  Size: 149 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 109 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 107 B

View File

@ -51,6 +51,7 @@ package org.bigbluebutton.modules.screenshare.managers
private var globalDispatcher:Dispatcher;
private var sharing:Boolean = false;
private var usingWebRTC:Boolean = false;
private var usingKurentoWebRTC:Boolean = false;
private var chromeExtensionKey:String = null;
private var options:ScreenshareOptions;
@ -109,7 +110,12 @@ package org.bigbluebutton.modules.screenshare.managers
publishWindowManager.stopSharing();
if (ExternalInterface.available) {
ExternalInterface.call("vertoExitScreenShare");
if(usingKurentoWebRTC) {
ExternalInterface.call("kurentoExitScreenShare");
}
else {
ExternalInterface.call("vertoExitScreenShare");
}
}
}
@ -126,16 +132,29 @@ package org.bigbluebutton.modules.screenshare.managers
var voiceBridge:String = UsersUtil.getVoiceBridge();
var myName:String = UsersUtil.getMyUsername();
var internalMeetingID:String = UsersUtil.getInternalMeetingID();
ExternalInterface.call(
'vertoShareScreen',
videoTag,
voiceBridge,
myName,
null,
"onFail",
chromeExtensionKey
);
if(usingKurentoWebRTC) {
ExternalInterface.call(
'kurentoShareScreen',
videoTag,
voiceBridge,
myName,
internalMeetingID,
"onFail",
chromeExtensionKey
);
} else {
ExternalInterface.call(
'vertoShareScreen',
videoTag,
voiceBridge,
myName,
null,
"onFail",
chromeExtensionKey
);
}
}
}
@ -147,6 +166,7 @@ package org.bigbluebutton.modules.screenshare.managers
chromeExtensionKey = options.chromeExtensionKey;
}
usingWebRTC = options.tryWebRTCFirst;
usingKurentoWebRTC = options.tryKurentoWebRTC;
}
public function handleMadePresenterEvent(e:MadePresenterEvent):void {
@ -170,6 +190,7 @@ package org.bigbluebutton.modules.screenshare.managers
LOGGER.debug("Cannot use WebRTC Screensharing: " + message);
JSLog.warn("Cannot use WebRTC Screensharing: ", message);
usingWebRTC = false;
usingKurentoWebRTC = false;
// send out event to fallback to Java
globalDispatcher.dispatchEvent(new UseJavaModeCommand());
};
@ -200,6 +221,7 @@ package org.bigbluebutton.modules.screenshare.managers
if (WebRTCAudioStatus.getInstance().getDidWebRTCAudioFail()) {
usingWebRTC = false;
usingKurentoWebRTC = false;
globalDispatcher.dispatchEvent(new UseJavaModeCommand());
return;
}
@ -242,6 +264,7 @@ package org.bigbluebutton.modules.screenshare.managers
public function handleUseJavaModeCommand():void {
LOGGER.debug("WebRTCDeskshareManager::handleUseJavaModeCommand");
usingWebRTC = false;
usingKurentoWebRTC = false;
}
public function handleRequestStartSharingEvent():void {

View File

@ -30,6 +30,9 @@ package org.bigbluebutton.modules.screenshare.model {
[Bindable]
public var tryWebRTCFirst:Boolean = false;
[Bindable]
public var tryKurentoWebRTC:Boolean = false;
[Bindable]
public var chromeExtensionLink:String = "";

View File

@ -164,6 +164,10 @@
private const LISTEN_TO_BREAKOUT_ROOM:String = "Listen To Breakout Room";
private const JOIN_BREAKOUT_ROOM:String = "Join Breakout Room";
private const STATUS_GRID_WIDTH:int = 56;
private const NAME_GRID_MIN_WIDTH:int = 80;
private const MEDIA_GRID_WIDTH:int = 112;
private var muteMeRolled:Boolean = false;
private var handler: UsersWindowEventHandler = new UsersWindowEventHandler();
@ -692,13 +696,14 @@
dragEnabled="false" width="100%" height="100%" draggableColumns="false"
itemRollOver="onItemRollOver(event)"
itemRollOut="onItemRollOut(event)"
minWidth="{STATUS_GRID_WIDTH + NAME_GRID_MIN_WIDTH + MEDIA_GRID_WIDTH}"
accessibilityName="{ResourceUtil.getInstance().getString('bbb.users.usersGrid.accessibilityName')}" >
<views:columns>
<mx:DataGridColumn dataField="userStatus" headerText="{ResourceUtil.getInstance().getString('bbb.users.usersGrid.statusItemRenderer')}" editable="false" width="56" resizable="false"
<mx:DataGridColumn dataField="userStatus" headerText="{ResourceUtil.getInstance().getString('bbb.users.usersGrid.statusItemRenderer')}" editable="false" width="{STATUS_GRID_WIDTH}" resizable="false"
itemRenderer="org.bigbluebutton.modules.users.views.StatusItemRenderer" sortable="false" />
<mx:DataGridColumn dataField="name" headerText="{ResourceUtil.getInstance().getString('bbb.users.usersGrid.nameItemRenderer')}" editable="false" sortable="false"
itemRenderer="org.bigbluebutton.modules.users.views.NameItemRenderer"/>
<mx:DataGridColumn dataField="media" headerText="{ResourceUtil.getInstance().getString('bbb.users.usersGrid.mediaItemRenderer')}" sortable="false" width="112" resizable="false"
<mx:DataGridColumn dataField="media" headerText="{ResourceUtil.getInstance().getString('bbb.users.usersGrid.mediaItemRenderer')}" sortable="false" width="{MEDIA_GRID_WIDTH}" resizable="false"
itemRenderer="org.bigbluebutton.modules.users.views.MediaItemRenderer"/>
</views:columns>
</views:BBBDataGrid>

View File

@ -0,0 +1,41 @@
/**
* BigBlueButton open source conferencing system - http://www.bigbluebutton.org/
*
* Copyright (c) 2012 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.modules.videoconf.events
{
import flash.events.Event;
import mx.core.IUIComponent;
public class AddStaticComponent extends Event
{
public static const ADD_STATIC_COMPONENT:String = "ADD_STATIC_COMPONENT";
private var _component:IUIComponent;
public function AddStaticComponent(component:IUIComponent)
{
super(ADD_STATIC_COMPONENT, true, false);
this._component = component;
}
public function get component():IUIComponent
{
return _component;
}
}
}

View File

@ -32,6 +32,7 @@ with BigBlueButton; if not, see <http://www.gnu.org/licenses/>.
import org.bigbluebutton.main.model.users.events.StreamStartedEvent;
import org.bigbluebutton.main.model.users.events.StreamStoppedEvent;
import org.bigbluebutton.modules.users.events.ViewCameraEvent;
import org.bigbluebutton.modules.videoconf.events.AddStaticComponent;
import org.bigbluebutton.modules.videoconf.events.ClosePublishWindowEvent;
import org.bigbluebutton.modules.videoconf.events.ConnectedEvent;
import org.bigbluebutton.modules.videoconf.events.ShareCameraRequestEvent;
@ -137,6 +138,10 @@ with BigBlueButton; if not, see <http://www.gnu.org/licenses/>.
<EventHandlers type="{BBBEvent.RECONNECT_DISCONNECTED_EVENT}">
<MethodInvoker generator="{VideoEventMapDelegate}" method="handleReconnectDisconnectedEvent" arguments="{event}"/>
</EventHandlers>
<EventHandlers type="{AddStaticComponent.ADD_STATIC_COMPONENT}">
<MethodInvoker generator="{VideoEventMapDelegate}" method="addStaticComponent" arguments="{[event.component]}" />
</EventHandlers>
<!-- ~~~~~~~~~~~~~~~~~~ INJECTORS ~~~~~~~~~~~~~~~~~~~~~~~~~~~ -->
</fx:Declarations>
</EventMap>

View File

@ -25,6 +25,7 @@ package org.bigbluebutton.modules.videoconf.maps
import mx.collections.ArrayCollection;
import mx.collections.ArrayList;
import mx.core.IUIComponent;
import org.as3commons.logging.api.ILogger;
import org.as3commons.logging.api.getClassLogger;
@ -109,6 +110,10 @@ package org.bigbluebutton.modules.videoconf.maps
_videoDock.addChild(_graphics);
}
public function addStaticComponent(component:IUIComponent):void {
_graphics.addStaticComponent(component);
}
public function viewCamera(userID:String, stream:String, name:String, mock:Boolean = false):void {
LOGGER.debug("VideoEventMapDelegate:: [{0}] viewCamera. ready = [{1}]", [me, _ready]);

View File

@ -3,8 +3,11 @@ package org.bigbluebutton.modules.videoconf.views
import flash.display.DisplayObject;
import flash.events.MouseEvent;
import flash.net.NetConnection;
import flash.utils.setTimeout;
import mx.containers.Canvas;
import mx.core.UIComponent;
import mx.core.IUIComponent;
import mx.events.FlexEvent;
import org.as3commons.logging.api.ILogger;
@ -308,6 +311,20 @@ package org.bigbluebutton.modules.videoconf.views
super.addChild(graphic);
}
public function addStaticComponent(component:IUIComponent):void {
component.addEventListener(MouseEvent.CLICK, onVBoxClick);
component.addEventListener(FlexEvent.REMOVE, onChildRemove);
setTimeout(onChildAdd, 150, null);
setTimeout(onChildAdd, 4000, null);
component.addEventListener(FlexEvent.CREATION_COMPLETE, function(event:FlexEvent):void {
onChildAdd(event);
});
super.addChild(component as DisplayObject);
}
private function onChildAdd(event:FlexEvent):void {
_minContentAspectRatio = minContentAspectRatio();
invalidateDisplayList();

View File

@ -34,12 +34,16 @@ with BigBlueButton; if not, see <http://www.gnu.org/licenses/>.
<mate:Listener type="{PresenterStatusEvent.PRESENTER_NAME_CHANGE}" method="handlePresenterChangedEvent" />
<mate:Listener type="{BBBEvent.USER_VOICE_LEFT}" method="handleUserVoiceChangedEvent" />
<mate:Listener type="{BBBEvent.USER_VOICE_MUTED}" method="handleUserVoiceChangedEvent" />
<mate:Listener type="{StoppedViewingWebcamEvent.STOPPED_VIEWING_WEBCAM}" method="handleStoppedViewingWebcamEvent" />
<mate:Listener type="{UserJoinedEvent.JOINED}" method="handleUserJoinedEvent" />
</fx:Declarations>
<fx:Script>
<![CDATA[
import com.asfusion.mate.events.Dispatcher;
import mx.core.UIComponent;
import org.bigbluebutton.common.Images;
import org.bigbluebutton.common.Role;
import org.bigbluebutton.core.EventConstants;
@ -51,8 +55,11 @@ with BigBlueButton; if not, see <http://www.gnu.org/licenses/>.
import org.bigbluebutton.core.model.VideoProfile;
import org.bigbluebutton.core.model.users.User2x;
import org.bigbluebutton.core.model.users.VoiceUser2x;
import org.bigbluebutton.main.events.StoppedViewingWebcamEvent;
import org.bigbluebutton.main.events.UserJoinedEvent;
import org.bigbluebutton.main.events.BBBEvent;
import org.bigbluebutton.main.events.PresenterStatusEvent;
import org.bigbluebutton.modules.videoconf.events.AddStaticComponent;
import org.bigbluebutton.modules.videoconf.model.VideoConfOptions;
import org.bigbluebutton.util.i18n.ResourceUtil;
@ -74,6 +81,11 @@ with BigBlueButton; if not, see <http://www.gnu.org/licenses/>.
private var _hideMuteBtnTimer:Timer;
private var _newParent:DisplayObjectContainer;
private var _oldParent:DisplayObjectContainer = null;
private var _oldW:Number = 100;
private var _oldH:Number = 100;
protected function init():void {
_hideMuteBtnTimer = new Timer(500, 1);
_hideMuteBtnTimer.addEventListener(TimerEvent.TIMER, onHideMuteBtnTimerComplete);
@ -81,7 +93,18 @@ with BigBlueButton; if not, see <http://www.gnu.org/licenses/>.
}
protected function onCreationComplete():void {
closeBtn.tabIndex = videoOptions.baseTabIndex + 10;
// There must be a better way!
_newParent = parent.parent.parent.parent.parent;
fullScreenIcon.source = _images.full_screen_white;
closeIcon.source = _images.close_white;
}
private function handleUserJoinedEvent(event:UserJoinedEvent):void {
// this is just to enforce the update of the BBBUser reference when the user reconnect
if (user && event.userID == user.intId) {
userId = event.userID;
setUserProperties();
}
}
public function set userId(value:String):void {
@ -160,6 +183,7 @@ with BigBlueButton; if not, see <http://www.gnu.org/licenses/>.
}
public function shutdown():void {
if (isFullscreen()) onFullscreen();
video.shutdown();
}
@ -310,6 +334,68 @@ with BigBlueButton; if not, see <http://www.gnu.org/licenses/>.
muteBtnWrapper.visible = true;
}
// Fullscreen video isn't a child of this module's wrapper so it must listen to it's close event
private function handleStoppedViewingWebcamEvent(event:StoppedViewingWebcamEvent):void {
if (user) {
if (user.intId == event.webcamUserID && video.streamName == event.streamName) {
handleCloseFullscreenStream();
}
}
}
private function handleCloseFullscreenStream():void {
if (isFullscreen()) {
shutdown();
}
}
private function isFullscreen():Boolean {
return _oldParent != null;
}
protected function onFullscreen(event:MouseEvent = null):void {
if (isFullscreen()) {
fullScreenIcon.source = _images.full_screen_white;
var evt:AddStaticComponent = new AddStaticComponent(this);
_dispatcher.dispatchEvent(evt);
_oldParent = null;
this.visible = true;
this.includeInLayout = true;
this.width = _oldW;
this.height = _oldH;
for(var i2:uint = 0; i2 < _newParent.numChildren; i2++) {
if(_newParent.getChildAt(i2) != this) {
_newParent.getChildAt(i2).visible = true;
(_newParent.getChildAt(i2) as UIComponent).includeInLayout = true;
}
}
} else {
fullScreenIcon.source = _images.full_screen_exit_white;
_oldParent = this.parent;
_oldW = this.width;
_oldH = this.height;
_newParent.addChildAt(this, 0);
this.percentHeight = 100;
this.percentWidth = 100;
for(var i1:uint = 0; i1< _newParent.numChildren; i1++) {
if(_newParent.getChildAt(i1) != this) {
_newParent.getChildAt(i1).visible = false;
(_newParent.getChildAt(i1) as UIComponent).includeInLayout = false;
}
}
}
if (event != null) {
event.stopImmediatePropagation();
event.stopPropagation();
}
}
]]>
</fx:Script>
@ -340,9 +426,11 @@ with BigBlueButton; if not, see <http://www.gnu.org/licenses/>.
minWidth="0"
truncateToFit="true"
styleName="videoToolbarLabelStyle" />
<mx:Box paddingRight="5">
<mx:Button id="closeBtn" styleName="closeBtnFocus" buttonMode="true" click="shutdown()"
toolTip="{ResourceUtil.getInstance().getString('bbb.video.streamClose.toolTip', [_username])}" />
<mx:Box paddingRight="0">
<mx:Image id="fullScreenIcon" buttonMode="true" click="onFullscreen(event)" />
</mx:Box>
<mx:Box paddingRight="3">
<mx:Image id="closeIcon" buttonMode="true" click="shutdown()" />
</mx:Box>
</mx:HBox>
<mx:Box

View File

@ -292,7 +292,7 @@ stop_bigbluebutton () {
if [ -f /usr/lib/systemd/system/bbb-webhooks.service ]; then
WEBHOOKS=bbb-webhooks
fi
systemctl stop red5 tomcat7 nginx freeswitch redis-server bbb-apps-akka bbb-fsesl-akka bbb-rap-archive-worker.service bbb-rap-process-worker.service bbb-rap-publish-worker.service bbb-rap-sanity-worker.service bbb-record-core.timer $HTML5 $WEBHOOKS
systemctl stop red5 tomcat7 nginx freeswitch redis-server bbb-apps-akka bbb-transcode-akka bbb-fsesl-akka bbb-rap-archive-worker.service bbb-rap-process-worker.service bbb-rap-publish-worker.service bbb-rap-sanity-worker.service bbb-record-core.timer $HTML5 $WEBHOOKS
else
/etc/init.d/monit stop
@ -325,6 +325,13 @@ stop_bigbluebutton () {
if [ -f /etc/init.d/bbb-fsesl-akka ]; then
/etc/init.d/bbb-fsesl-akka stop
fi
if [ -f /etc/init/bbb-transcode-akka.conf ]; then
service bbb-transcode-akka stop
fi
if [ -f /etc/init.d/bbb-transcode-akka ]; then
/etc/init.d/bbb-transcode-akka stop
fi
fi
}
@ -337,7 +344,7 @@ start_bigbluebutton () {
if [ -f /usr/lib/systemd/system/bbb-webhooks.service ]; then
WEBHOOKS=bbb-webhooks
fi
systemctl start red5 tomcat7 nginx freeswitch redis-server bbb-apps-akka bbb-fsesl-akka bbb-record-core.timer $HTML5 $WEBHOOKS
systemctl start red5 tomcat7 nginx freeswitch redis-server bbb-apps-akka bbb-transcode-akka bbb-fsesl-akka bbb-record-core.timer $HTML5 $WEBHOOKS
else
$FREESWITCH_INIT_D start
@ -391,6 +398,13 @@ start_bigbluebutton () {
/etc/init.d/bbb-fsesl-akka start
fi
if [ -f /etc/init/bbb-transcode-akka.conf ]; then
service bbb-transcode-akka start
fi
if [ -f /etc/init.d/bbb-transcode-akka ]; then
/etc/init.d/bbb-transcode-akka start
fi
#
# At this point the red5 and servlet container applications are starting up.
@ -453,7 +467,7 @@ start_bigbluebutton () {
display_bigbluebutton_status () {
if command -v systemctl >/dev/null; then
units="start red5 tomcat7 nginx freeswitch redis-server libreoffice bbb-apps-akka bbb-fsesl-akka"
units="start red5 tomcat7 nginx freeswitch redis-server libreoffice bbb-apps-akka bbb-transcode-akka bbb-fsesl-akka"
for unit in $units; do
echo "$unit: $(systemctl is-active $unit)"
done
@ -1576,6 +1590,7 @@ if [ $ZIP ]; then
tar rfh /tmp/$LOG_FILE.tar $SERVLET_LOGS > /dev/null 2>&1
tar rf /tmp/$LOG_FILE.tar /var/log/bigbluebutton/* > /dev/null 2>&1
tar rf /tmp/$LOG_FILE.tar /var/log/bbb-apps-akka > /dev/null 2>&1
tar rf /tmp/$LOG_FILE.tar /var/log/bbb-transcode-akka > /dev/null 2>&1
tar rf /tmp/$LOG_FILE.tar /var/log/bbb-fsesl-akka > /dev/null 2>&1
tar rf /tmp/$LOG_FILE.tar /var/log/nginx/error.log > /dev/null 2>&1
if [ $DISTRIB_ID == "ubuntu" ]; then
@ -1877,6 +1892,10 @@ if [ $CLEAN ]; then
rm -f /var/log/bbb-apps-akka/*
fi
if [ -d /var/log/bbb-transcode-akka ]; then
rm -f /var/log/bbb-transcode-akka/*
fi
start_bigbluebutton
check_state
fi

View File

@ -20,6 +20,7 @@ task resolveDeps(type: Copy) {
*/
repositories {
mavenCentral()
mavenLocal()
add(new org.apache.ivy.plugins.resolver.ChainResolver()) {
name = 'remote'
returnFirst = true
@ -138,6 +139,8 @@ dependencies {
compile 'redis.clients:jedis:1.5.1'
providedCompile 'commons-pool:commons-pool:1.5.6'
compile 'com.google.code.gson:gson:1.7.1'
compile 'org.bigbluebutton:bbb-common-message:0.0.16'
}
test {

View File

@ -0,0 +1,138 @@
/**
* BigBlueButton open source conferencing system - http://www.bigbluebutton.org/
*
* Copyright (c) 2017 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.deskshare.server.red5.pubsub;
import org.apache.commons.pool.impl.GenericObjectPool;
public class GenericObjectPoolConfigWrapper {
private final GenericObjectPool.Config config;
public GenericObjectPoolConfigWrapper() {
this.config = new GenericObjectPool.Config();
}
public GenericObjectPool.Config getConfig() {
return config;
}
public int getMaxIdle() {
return this.config.maxIdle;
}
public void setMaxIdle(int maxIdle) {
this.config.maxIdle = maxIdle;
}
public int getMinIdle() {
return this.config.minIdle;
}
public void setMinIdle(int minIdle) {
this.config.minIdle = minIdle;
}
public int getMaxActive() {
return this.config.maxActive;
}
public void setMaxActive(int maxActive) {
this.config.maxActive = maxActive;
}
public long getMaxWait() {
return this.config.maxWait;
}
public void setMaxWait(long maxWait) {
this.config.maxWait = maxWait;
}
public byte getWhenExhaustedAction() {
return this.config.whenExhaustedAction;
}
public void setWhenExhaustedAction(byte whenExhaustedAction) {
this.config.whenExhaustedAction = whenExhaustedAction;
}
public boolean isTestOnBorrow() {
return this.config.testOnBorrow;
}
public void setTestOnBorrow(boolean testOnBorrow) {
this.config.testOnBorrow = testOnBorrow;
}
public boolean isTestOnReturn() {
return this.config.testOnReturn;
}
public void setTestOnReturn(boolean testOnReturn) {
this.config.testOnReturn = testOnReturn;
}
public boolean isTestWhileIdle() {
return this.config.testWhileIdle;
}
public void setTestWhileIdle(boolean testWhileIdle) {
this.config.testWhileIdle = testWhileIdle;
}
public long getTimeBetweenEvictionRunsMillis() {
return this.config.timeBetweenEvictionRunsMillis;
}
public void setTimeBetweenEvictionRunsMillis(long timeBetweenEvictionRunsMillis) {
this.config.timeBetweenEvictionRunsMillis = timeBetweenEvictionRunsMillis;
}
public int getNumTestsPerEvictionRun() {
return this.config.numTestsPerEvictionRun;
}
public void setNumTestsPerEvictionRun(int numTestsPerEvictionRun) {
this.config.numTestsPerEvictionRun = numTestsPerEvictionRun;
}
public long getMinEvictableIdleTimeMillis() {
return this.config.minEvictableIdleTimeMillis;
}
public void setMinEvictableIdleTimeMillis(long minEvictableIdleTimeMillis) {
this.config.minEvictableIdleTimeMillis = minEvictableIdleTimeMillis;
}
public long getSoftMinEvictableIdleTimeMillis() {
return this.config.softMinEvictableIdleTimeMillis;
}
public void setSoftMinEvictableIdleTimeMillis(long softMinEvictableIdleTimeMillis) {
this.config.softMinEvictableIdleTimeMillis = softMinEvictableIdleTimeMillis;
}
public boolean isLifo() {
return this.config.lifo;
}
public void setLifo(boolean lifo) {
this.config.lifo = lifo;
}
}

View File

@ -0,0 +1,55 @@
/**
* BigBlueButton open source conferencing system - http://www.bigbluebutton.org/
*
* Copyright (c) 2017 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.deskshare.server.red5.pubsub;
import java.util.Map;
import java.util.HashMap;
import org.bigbluebutton.common.messages.MessagingConstants;
import org.bigbluebutton.common.messages.Constants;
import org.bigbluebutton.common.messages.StartTranscoderRequestMessage;
import org.bigbluebutton.common.messages.StopTranscoderRequestMessage;
import org.bigbluebutton.deskshare.server.red5.pubsub.MessageSender;
public class MessagePublisher {
private MessageSender sender;
public void setMessageSender(MessageSender sender) {
this.sender = sender;
}
public void startH264ToH263TranscoderRequest(String meetingId, String streamName, String ipAddress) {
Map<String, String> params = new HashMap<String, String>();
params.put(Constants.TRANSCODER_TYPE, Constants.TRANSCODE_H264_TO_H263);
params.put(Constants.MODULE, "deskShare");
params.put(Constants.LOCAL_IP_ADDRESS, ipAddress);
params.put(Constants.DESTINATION_IP_ADDRESS, ipAddress);
params.put(Constants.INPUT, streamName);
// TODO: transcoderId is getting meetingId, this may have to change
StartTranscoderRequestMessage msg = new StartTranscoderRequestMessage(meetingId, meetingId, params);
sender.send(MessagingConstants.TO_BBB_TRANSCODE_SYSTEM_CHAN, msg.toJson());
}
public void stopTranscoderRequest(String meetingId) {
// TODO: transcoderId is getting meetingId, this may have to change
StopTranscoderRequestMessage msg = new StopTranscoderRequestMessage(meetingId, meetingId);
sender.send(MessagingConstants.TO_BBB_TRANSCODE_SYSTEM_CHAN, msg.toJson());
}
}

View File

@ -0,0 +1,94 @@
/**
* BigBlueButton open source conferencing system - http://www.bigbluebutton.org/
*
* Copyright (c) 2017 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.deskshare.server.red5.pubsub;
import java.util.concurrent.BlockingQueue;
import java.util.concurrent.Executor;
import java.util.concurrent.Executors;
import java.util.concurrent.LinkedBlockingQueue;
import org.red5.logging.Red5LoggerFactory;
import org.slf4j.Logger;
import redis.clients.jedis.Jedis;
import redis.clients.jedis.JedisPool;
public class MessageSender {
private static Logger log = Red5LoggerFactory.getLogger(MessageSender.class, "deskshare");
private JedisPool redisPool;
private volatile boolean sendMessage = false;
private final Executor msgSenderExec = Executors.newSingleThreadExecutor();
private final Executor runExec = Executors.newSingleThreadExecutor();
private BlockingQueue<MessageToSend> messages = new LinkedBlockingQueue<MessageToSend>();
public void stop() {
sendMessage = false;
}
public void start() {
log.info("Redis message publisher starting!");
try {
sendMessage = true;
Runnable messageSender = new Runnable() {
public void run() {
while (sendMessage) {
try {
MessageToSend msg = messages.take();
publish(msg.getChannel(), msg.getMessage());
} catch (InterruptedException e) {
log.warn("Failed to get message from queue.");
}
}
}
};
msgSenderExec.execute(messageSender);
} catch (Exception e) {
log.error("Error subscribing to channels: " + e.getMessage());
}
}
public void send(String channel, String message) {
MessageToSend msg = new MessageToSend(channel, message);
messages.add(msg);
}
private void publish(final String channel, final String message) {
Runnable task = new Runnable() {
public void run() {
Jedis jedis = redisPool.getResource();
try {
jedis.publish(channel, message);
} catch(Exception e){
log.warn("Cannot publish the message to redis", e);
} finally {
redisPool.returnResource(jedis);
}
}
};
runExec.execute(task);
}
public void setRedisPool(JedisPool redisPool){
this.redisPool = redisPool;
}
}

View File

@ -0,0 +1,37 @@
/**
* BigBlueButton open source conferencing system - http://www.bigbluebutton.org/
*
* Copyright (c) 2017 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.deskshare.server.red5.pubsub;
public class MessageToSend {
private final String channel;
private final String message;
public MessageToSend(String channel, String message) {
this.channel = channel;
this.message = message;
}
public String getChannel() {
return channel;
}
public String getMessage() {
return message;
}
}

View File

@ -27,6 +27,7 @@ import org.bigbluebutton.deskshare.server.RtmpClientAdapter
import org.bigbluebutton.deskshare.server.stream.StreamManager
import org.bigbluebutton.deskshare.server.socket.DeskShareServer
import org.bigbluebutton.deskshare.server.MultiThreadedAppAdapter
import org.bigbluebutton.deskshare.server.red5.pubsub.MessagePublisher
import scala.actors.Actor
import scala.actors.Actor._
import net.lag.configgy.Configgy
@ -37,7 +38,7 @@ import org.red5.server.api.scope.{IScope}
import org.red5.server.util.ScopeUtils
import com.google.gson.Gson
class DeskshareApplication(streamManager: StreamManager, deskShareServer: DeskShareServer) extends MultiThreadedAppAdapter {
class DeskshareApplication(streamManager: StreamManager, deskShareServer: DeskShareServer, messagePublisher: MessagePublisher) extends MultiThreadedAppAdapter {
private val deathSwitch = new CountDownLatch(1)
// load our config file and configure logfiles.

View File

@ -0,0 +1,64 @@
<?xml version="1.0" encoding="UTF-8"?>
<!--
BigBlueButton open source conferencing system - http://www.bigbluebutton.org/
Copyright (c) 2012 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/>.
-->
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:util="http://www.springframework.org/schema/util"
xsi:schemaLocation="http://www.springframework.org/schema/beans
http://www.springframework.org/schema/beans/spring-beans-2.5.xsd
http://www.springframework.org/schema/util
http://www.springframework.org/schema/util/spring-util-2.0.xsd">
<bean id="redisPool" class="redis.clients.jedis.JedisPool">
<constructor-arg index="0">
<bean factory-bean="config" factory-method="getConfig" />
</constructor-arg>
<constructor-arg index="1" value="${redis.host}"/>
<constructor-arg index="2" value="${redis.port}"/>
</bean>
<bean id="config" class="org.bigbluebutton.deskshare.server.red5.pubsub.GenericObjectPoolConfigWrapper">
<!-- Action to take when trying to acquire a connection and all connections are taken -->
<property name="whenExhaustedAction">
<!-- Fail-fast behaviour, we don't like to keep the kids waiting -->
<util:constant static-field="org.apache.commons.pool.impl.GenericObjectPool.WHEN_EXHAUSTED_FAIL"/>
<!-- Default behaviour, block the caller until a resource becomes available -->
<!--<util:constant static-field="org.apache.commons.pool.impl.GenericObjectPool.WHEN_EXHAUSTED_BLOCK" />-->
</property>
<!-- Maximum active connections to Redis instance -->
<property name="maxActive" value="12"/>
<!-- Number of connections to Redis that just sit there and do nothing -->
<property name="maxIdle" value="6"/>
<!-- Minimum number of idle connections to Redis - these can be seen as always open and ready to serve -->
<property name="minIdle" value="1"/>
<!-- Tests whether connection is dead when connection retrieval method is called -->
<property name="testOnBorrow" value="true"/>
<!-- Tests whether connection is dead when returning a connection to the pool -->
<property name="testOnReturn" value="true"/>
<!-- Tests whether connections are dead during idle periods -->
<property name="testWhileIdle" value="true"/>
<!-- Maximum number of connections to test in each idle check -->
<property name="numTestsPerEvictionRun" value="12"/>
<!-- Idle connection checking period -->
<property name="timeBetweenEvictionRunsMillis" value="60000"/>
<!-- Maximum time, in milliseconds, to wait for a resource when exausted action is set to WHEN_EXAUSTED_BLOCK -->
<property name="maxWait" value="5000"/>
</bean>
</beans>

View File

@ -39,6 +39,7 @@ with BigBlueButton; if not, see <http://www.gnu.org/licenses/>.
<bean id="web.handler" class="org.bigbluebutton.deskshare.server.red5.DeskshareApplication">
<constructor-arg ref="streamManager"/>
<constructor-arg ref="deskShareServer"/>
<constructor-arg ref="messagePublisher"/>
</bean>
<bean id="deskshare.service" class="org.bigbluebutton.deskshare.server.stream.DeskshareService">
<constructor-arg ref="streamManager"/>
@ -71,4 +72,12 @@ with BigBlueButton; if not, see <http://www.gnu.org/licenses/>.
<constructor-arg index="1" value="${redis.port}"/>
<constructor-arg index="2" value="${redis.keyExpiry}"/>
</bean>
<bean id="messageSender" class="org.bigbluebutton.deskshare.server.red5.pubsub.MessageSender"
init-method="start" destroy-method="stop">
<property name="redisPool" ref="redisPool"/>
</bean>
<bean id="messagePublisher" class="org.bigbluebutton.deskshare.server.red5.pubsub.MessagePublisher">
<property name="messageSender" ref="messageSender"/>
</bean>
<import resource="bbb-redis-pool.xml"/>
</beans>

1
labs/kurento-screenshare/.gitignore vendored Normal file
View File

@ -0,0 +1 @@
node_modules/

View File

View File

@ -0,0 +1,8 @@
kurentoUrl: "KURENTOURL"
kurentoIp: "KURENTOIP"
localIpAddress: "HOST"
acceptSelfSignedCertificate: false
redisHost : "127.0.0.1"
redisPort : "6379"
minVideoPort: 30000
maxVideoPort: 33000

View File

@ -0,0 +1 @@
node --inspect --debug-brk server.js

View File

@ -0,0 +1,2 @@
This folder contains a dummy self-signed certificate only for demo purposses,
**DON'T USE IT IN PRODUCTION**.

View File

@ -0,0 +1,19 @@
-----BEGIN CERTIFICATE-----
MIIDBjCCAe4CCQCuf5QfyX2oDDANBgkqhkiG9w0BAQsFADBFMQswCQYDVQQGEwJB
VTETMBEGA1UECAwKU29tZS1TdGF0ZTEhMB8GA1UECgwYSW50ZXJuZXQgV2lkZ2l0
cyBQdHkgTHRkMB4XDTE0MDkyOTA5NDczNVoXDTE1MDkyOTA5NDczNVowRTELMAkG
A1UEBhMCQVUxEzARBgNVBAgMClNvbWUtU3RhdGUxITAfBgNVBAoMGEludGVybmV0
IFdpZGdpdHMgUHR5IEx0ZDCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEB
AMJOyOHJ+rJWJEQ7P7kKoWa31ff7hKNZxF6sYE5lFi3pBYWIY6kTN/iUaxJLROFo
FhoC/M/STY76rIryix474v/6cRoG8N+GQBEn4IAP1UitWzVO6pVvBaIt5IKlhhfm
YA1IMweCd03vLcaHTddNmFDBTks7QDwfenTaR5VjKYc3OtEhcG8dgLAnOjbbk2Hr
8wter2IeNgkhya3zyoXnTLT8m8IMg2mQaJs62Xlo9gs56urvVDWG4rhdGybj1uwU
ZiDYyP4CFCUHS6UVt12vADP8vjbwmss2ScGsIf0NjaU+MpSdEbB82z4b2NiN8Wq+
rFA/JbvyeoWWHMoa7wkVs1MCAwEAATANBgkqhkiG9w0BAQsFAAOCAQEAYLRwV9fo
AOhJfeK199Tv6oXoNSSSe10pVLnYxPcczCVQ4b9SomKFJFbmwtPVGi6w3m+8mV7F
9I2WKyeBHzmzfW2utZNupVybxgzEjuFLOVytSPdsB+DcJomOi8W/Cf2Vk8Wykb/t
Ctr1gfOcI8rwEGKxm279spBs0u1snzoLyoimbMbiXbC82j1IiN3Jus08U07m/j7N
hRBCpeHjUHT3CRpvYyTRnt+AyBd8BiyJB7nWmcNI1DksXPfehd62MAFS9e1ZE+dH
Aavg/U8VpS7pcCQcPJvIJ2hehrt8L6kUk3YUYqZ0OeRZK27f2R5+wFlDF33esm3N
dCSsLJlXyqAQFg==
-----END CERTIFICATE-----

View File

@ -0,0 +1,16 @@
-----BEGIN CERTIFICATE REQUEST-----
MIICijCCAXICAQAwRTELMAkGA1UEBhMCQVUxEzARBgNVBAgMClNvbWUtU3RhdGUx
ITAfBgNVBAoMGEludGVybmV0IFdpZGdpdHMgUHR5IEx0ZDCCASIwDQYJKoZIhvcN
AQEBBQADggEPADCCAQoCggEBAMJOyOHJ+rJWJEQ7P7kKoWa31ff7hKNZxF6sYE5l
Fi3pBYWIY6kTN/iUaxJLROFoFhoC/M/STY76rIryix474v/6cRoG8N+GQBEn4IAP
1UitWzVO6pVvBaIt5IKlhhfmYA1IMweCd03vLcaHTddNmFDBTks7QDwfenTaR5Vj
KYc3OtEhcG8dgLAnOjbbk2Hr8wter2IeNgkhya3zyoXnTLT8m8IMg2mQaJs62Xlo
9gs56urvVDWG4rhdGybj1uwUZiDYyP4CFCUHS6UVt12vADP8vjbwmss2ScGsIf0N
jaU+MpSdEbB82z4b2NiN8Wq+rFA/JbvyeoWWHMoa7wkVs1MCAwEAAaAAMA0GCSqG
SIb3DQEBCwUAA4IBAQBMszYHMpklgTF/3h1zAzKXUD9NrtZp8eWhL06nwVjQX8Ai
EaCUiW0ypstokWcH9+30chd2OD++67NbxYUEucH8HrKpOoy6gs5L/mqgQ9Npz3OT
TB1HI4kGtpVuUQ5D7L0596tKzMX/CgW/hRcHWl+PDkwGhQs1qZcJ8QN+YP6AkRrO
5sDdDB/BLrB9PtBQbPrYIQcHQ7ooYWz/G+goqRxzZ6rt0aU2uAB6l7c82ADLAqFJ
qlw+xqVzEETVfqM5TXKK/wV3hgm4oSX5Q4SHLKF94ODOkWcnV4nfIKz7y+5XcQ3p
PrGimI1br07okC5rO9cgLCR0Ks20PPFcM0FvInW/
-----END CERTIFICATE REQUEST-----

Some files were not shown because too many files have changed in this diff Show More