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:
commit
e11d69624f
@ -191,7 +191,6 @@ class BigBlueButtonActor(
|
||||
context.stop(m.actorRef)
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
@ -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)
|
||||
|
@ -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)
|
||||
|
@ -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
49
akka-bbb-transcode/.gitignore
vendored
Normal 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/
|
12
akka-bbb-transcode/README.md
Normal file
12
akka-bbb-transcode/README.md
Normal 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
97
akka-bbb-transcode/build.sbt
Executable 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")
|
0
akka-bbb-transcode/project/Build.scala
Executable file
0
akka-bbb-transcode/project/Build.scala
Executable file
1
akka-bbb-transcode/project/build.properties
Executable file
1
akka-bbb-transcode/project/build.properties
Executable file
@ -0,0 +1 @@
|
||||
sbt.version=0.13.8
|
7
akka-bbb-transcode/project/plugins.sbt
Executable file
7
akka-bbb-transcode/project/plugins.sbt
Executable 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")
|
@ -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;
|
||||
}
|
||||
|
||||
}
|
@ -0,0 +1,6 @@
|
||||
package org.bigbluebutton.transcode.api;
|
||||
|
||||
public class DestroyVideoTranscoderRequest extends InternalMessage {
|
||||
public DestroyVideoTranscoderRequest() {}
|
||||
|
||||
}
|
@ -0,0 +1,3 @@
|
||||
package org.bigbluebutton.transcode.api;
|
||||
|
||||
public class InternalMessage {}
|
@ -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;
|
||||
}
|
||||
|
||||
}
|
@ -0,0 +1,5 @@
|
||||
package org.bigbluebutton.transcode.api;
|
||||
|
||||
public class RestartVideoTranscoderRequest extends InternalMessage {
|
||||
public RestartVideoTranscoderRequest() {}
|
||||
}
|
@ -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;
|
||||
}
|
||||
}
|
@ -0,0 +1,5 @@
|
||||
package org.bigbluebutton.transcode.api;
|
||||
|
||||
public class StartVideoProbingRequest extends InternalMessage {
|
||||
public StartVideoProbingRequest() {}
|
||||
}
|
@ -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;
|
||||
}
|
||||
|
||||
}
|
@ -0,0 +1,5 @@
|
||||
package org.bigbluebutton.transcode.api;
|
||||
|
||||
public class StartVideoTranscoderRequest extends InternalMessage {
|
||||
public StartVideoTranscoderRequest() {}
|
||||
}
|
@ -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;
|
||||
}
|
||||
|
||||
}
|
@ -0,0 +1,5 @@
|
||||
package org.bigbluebutton.transcode.api;
|
||||
|
||||
public class StopVideoTranscoderRequest extends InternalMessage {
|
||||
public StopVideoTranscoderRequest() {}
|
||||
}
|
@ -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;
|
||||
}
|
||||
}
|
@ -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;
|
||||
}
|
||||
}
|
@ -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;
|
||||
}
|
||||
}
|
@ -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;
|
||||
}
|
||||
}
|
@ -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;
|
||||
}
|
||||
|
||||
}
|
@ -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);
|
||||
}
|
@ -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);
|
||||
}
|
@ -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");
|
||||
}
|
||||
}
|
@ -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;
|
||||
}
|
||||
}
|
@ -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;
|
||||
}
|
||||
}
|
@ -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);
|
||||
}
|
||||
|
||||
}
|
@ -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;
|
||||
}
|
||||
|
||||
}
|
@ -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);
|
||||
}
|
@ -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);
|
||||
}
|
||||
}
|
@ -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);
|
||||
}
|
||||
}
|
45
akka-bbb-transcode/src/main/resources/application.conf
Normal file
45
akka-bbb-transcode/src/main/resources/application.conf
Normal 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
|
||||
}
|
29
akka-bbb-transcode/src/main/resources/logback.xml
Normal file
29
akka-bbb-transcode/src/main/resources/logback.xml
Normal 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>
|
@ -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")
|
||||
}
|
@ -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("")
|
||||
}
|
@ -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. ****")
|
||||
}
|
||||
}
|
@ -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)
|
||||
}
|
||||
|
||||
}
|
@ -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)
|
||||
}
|
||||
|
||||
}
|
@ -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
|
@ -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
|
@ -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())
|
||||
}
|
||||
|
||||
}
|
@ -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
|
||||
}
|
||||
}
|
||||
}
|
@ -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
|
||||
}
|
||||
}
|
@ -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()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
@ -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");
|
||||
}
|
||||
}
|
||||
|
||||
}
|
@ -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";
|
||||
}
|
||||
|
@ -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";
|
||||
|
@ -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;
|
||||
}
|
||||
}
|
@ -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;
|
||||
}
|
||||
}
|
@ -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;
|
||||
}
|
||||
}
|
@ -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;
|
||||
}
|
||||
}
|
@ -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;
|
||||
}
|
||||
}
|
@ -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;
|
||||
}
|
||||
}
|
@ -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;
|
||||
}
|
||||
}
|
@ -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;
|
||||
}
|
||||
}
|
@ -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;
|
||||
}
|
||||
}
|
@ -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;
|
||||
}
|
||||
}
|
@ -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 {
|
||||
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
@ -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;
|
||||
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -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());
|
||||
}
|
||||
}
|
||||
|
@ -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
|
||||
|
@ -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
|
||||
|
@ -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>
|
||||
|
||||
|
433
bigbluebutton-client/resources/prod/lib/kurento-extension.js
Normal file
433
bigbluebutton-client/resources/prod/lib/kurento-extension.js
Normal 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);
|
||||
};
|
56
bigbluebutton-client/resources/prod/lib/kurento-utils.min.js
vendored
Normal file
56
bigbluebutton-client/resources/prod/lib/kurento-utils.min.js
vendored
Normal file
File diff suppressed because one or more lines are too long
@ -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 |
@ -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 {
|
||||
|
@ -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 = "";
|
||||
|
||||
|
@ -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>
|
||||
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
@ -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>
|
||||
|
@ -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]);
|
||||
|
||||
|
@ -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();
|
||||
|
@ -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
|
||||
|
@ -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
|
||||
|
@ -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 {
|
||||
|
@ -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;
|
||||
}
|
||||
}
|
@ -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());
|
||||
}
|
||||
}
|
@ -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;
|
||||
}
|
||||
}
|
@ -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;
|
||||
}
|
||||
}
|
@ -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.
|
||||
|
64
deskshare/app/src/main/webapp/WEB-INF/bbb-redis-pool.xml
Normal file
64
deskshare/app/src/main/webapp/WEB-INF/bbb-redis-pool.xml
Normal 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>
|
@ -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
1
labs/kurento-screenshare/.gitignore
vendored
Normal file
@ -0,0 +1 @@
|
||||
node_modules/
|
0
labs/kurento-screenshare/README.md
Normal file
0
labs/kurento-screenshare/README.md
Normal file
8
labs/kurento-screenshare/config/default.yml
Normal file
8
labs/kurento-screenshare/config/default.yml
Normal file
@ -0,0 +1,8 @@
|
||||
kurentoUrl: "KURENTOURL"
|
||||
kurentoIp: "KURENTOIP"
|
||||
localIpAddress: "HOST"
|
||||
acceptSelfSignedCertificate: false
|
||||
redisHost : "127.0.0.1"
|
||||
redisPort : "6379"
|
||||
minVideoPort: 30000
|
||||
maxVideoPort: 33000
|
1
labs/kurento-screenshare/debug-start.sh
Normal file
1
labs/kurento-screenshare/debug-start.sh
Normal file
@ -0,0 +1 @@
|
||||
node --inspect --debug-brk server.js
|
2
labs/kurento-screenshare/keys/README.md
Normal file
2
labs/kurento-screenshare/keys/README.md
Normal file
@ -0,0 +1,2 @@
|
||||
This folder contains a dummy self-signed certificate only for demo purposses,
|
||||
**DON'T USE IT IN PRODUCTION**.
|
19
labs/kurento-screenshare/keys/server.crt
Normal file
19
labs/kurento-screenshare/keys/server.crt
Normal 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-----
|
16
labs/kurento-screenshare/keys/server.csr
Normal file
16
labs/kurento-screenshare/keys/server.csr
Normal 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
Loading…
Reference in New Issue
Block a user