Writing a Red5 Java Web App to Handle RTMP Streams
After finally getting Red5 set up on my Ubuntu box, I discovered that it's completely useless to me.
Basically, I found out that the university won't allow any unsolicited incoming connections to computers with dynamic IPs on its network. This includes all of my computers. Naturally I didn't find this out until I tried to work on my project from Starbucks, so it was already too late to change much before my project was due. I ended up just buying a Linode virtual server for $20/month. Hopefully I won't need it for too long since I really don't like paying for something I already have. It was SUPER easy to set up though, so I'd definitely recommend Linode if you're looking for a VPS service. Getting Ubuntu running on my server seriously took less than 10 minutes.
I also learned that Red5 1.0 doesn't quite work right yet (at least, not the way I was using it), so I had to go back and change a few things to make it work with version 0.9. All of my code is written to work with Red5 version 0.9 and it will not work with version 1.0 without some minor modifications to reflect the changes made to the API over the last couple of years.
Also of note: the application I developed is only intended to record audio streams, not video. There's no reason why this code shouldn't also be able to record video, but I never tested it so I have no idea. The output of this application is a bunch of .flv files in its /streams folder, so I also had to write a little shell script to convert them all to .flac files, but that's a post for another time.
Anyway, this post will document (mostly for my own future reference) how to get a very basic Java app running on Red5 to handle RTMP streams. Before I start, here are some links to the code my solution is (heavily) based on:
- http://sziebert.net/posts/server-side-stream-recording-with-red5/
- http://www.freedevelopment.net/articles/red5-first-application-example.html
How Java Web Apps Work
If you've worked with GlassFish before then you should already be familiar with the Java Web Archive (.war) format. Red5 handles applications in exactly the same way but with a little extra metadata. The structure of the Red5 installation directory (where the web applications end up) looks something like this:
red5/ conf/ lib/ log/ plugins/ webapps/ appname/ persistence/ streams/ 1.flv 2.flv WEB-INF/ classes/ [various folders]/ Application.class [other .class files] red5-web.properties red5-web.xml web.xml red5default.xml red5.jar
Each web app has its own folder in /webapps consisting of the two .xml files, the .properties file, and its Java classes. The XML and .properties files just describe the web app and tell the server where to find all the parts it needs. You'll probably want some kind of IDE to manage all of these files though since it makes quite a mess if you try to do it yourself for anything more complicated than one or two classes.
Application.class is basically just a standard Java class that has a few specific methods that the server calls to handle requests. In GlassFish apps, these are usually HTTP requests, but with Red5 we're handling mostly RTMP streams. This requires slightly different logic, but all of the relevant methods are documented in the Red5 API and it's not very hard to grasp at all if you're familiar with Java.
Setting Up Eclipse
I decided to use Eclipse to develop the Java part of this project because it was the first editor I found instructions for. I hate pretty much all of the popular Java IDEs (especially Netbeans), so if you use a different one this part will be of no use to you. These instructions have been shamelessly copied from here and modified to work for the Windows version of Eclipse 4.2.1.
- Install Eclipse.
- Create new Java Project in Eclipse.
- Delete the src folder from the project.
- Create a WEB-INF folder in the project.
- WEB-INF should have the following sub-folders: src, classes
- Right click on WEB-INF/src and choose Build Path -> Use as Source Folder.
- Right click on WEB-INF/src and choose Build Path -> Configure Output Folder. Put "WEB-INF/classes" in the "Specify output folder" field.
- Right click on Project and choose Properties from the menu. Click on Java Build Path and choose the "Libraries" tab.
- Click on Add External JARs. You should add red5.jar from your red5 directory (/usr/share/red5/red5.jar for Ubuntu or C:\Program Files\Red5\red5.jar for Windows). You may add all the files from lib directory (/usr/share/red5/lib) as External JARs as well to let your new application see all available Red5 classes an methods. You should also make sure the Java SDK version you're using matches the one installed on the server running Red5.
- Right click on WEB-INF/src and choose New -> Class. Put something like "net.projectname" in the Package field and Application as the class name.
- Create three files in the WEB-INF directory: red5-web.properties, red5-web.xml, and web.xml.
My project has two .java files, Application.java and StreamManager.java. Here are their contents:
Application.java
package net.acsurvey;
//log4j classes
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
//Red5 classes
import org.red5.server.adapter.ApplicationAdapter;
import org.red5.server.api.IConnection;
public class Application extends ApplicationAdapter
{
// Handle to the log class
private static final Log log = LogFactory.getLog(Application.class);
// A user connects
@Override
public boolean roomConnect(IConnection conn, Object[] params)
{
log.info("New connection attempt from " + conn.getRemoteAddress());
// Ensure that the listeners are properly attached.
return super.roomConnect(conn, params);
}
// User disconnects
@Override
public void roomDisconnect(IConnection conn)
{
log.info("Connection closed by " + conn.getRemoteAddress());
// Call the super class to insure that all listeners are properly dismissed.
super.roomDisconnect(conn);
}
}
StreamManager.java
package net.acsurvey;
import java.util.Date;
import java.text.SimpleDateFormat;
//log4j classes
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
//Red5 classes
import org.red5.server.api.stream.IBroadcastStream;
import org.red5.server.api.IConnection;
import org.red5.server.api.Red5;
import org.red5.server.stream.ClientBroadcastStream;
// Manages streamed audio from clients
public class StreamManager {
// Handle logs
private static final Log log = LogFactory.getLog(Application.class);
// Handled connections
private Application app;
// Start recording -- open a file and name it after the user's IP and the current timestamp
public void startRecording()
{
IConnection clientConnection = Red5.getConnectionLocal(); // Get the connection
String clientIP = clientConnection.getRemoteAddress(); // Get the user's IP
SimpleDateFormat format = new SimpleDateFormat("yyyy-MM-dd_HH.mm.ss"); // Create a date formatter
String streamFileName = clientIP + "." + format.format(new Date()); // Format the current date
IBroadcastStream currentStream = app.getBroadcastStream(clientConnection.getScope(), "hostStream"); // Get the current stream
log.info("Recording stream from client " + clientIP); // Make a note in the log
// Try to save the stream to disk
try
{
currentStream.saveAs(streamFileName, false);
log.info("Stream is being saved to " + streamFileName);
} catch(Exception e) {
log.error("Unable to save stream."); // There's not really much else I care to do about this
}
}
// Stop recording -- close the file and the stream
public void stopRecording()
{
log.info("Recording stopped.");
IConnection clientConnection = Red5.getConnectionLocal(); // Get the connection
ClientBroadcastStream clientStream = (ClientBroadcastStream) app.getBroadcastStream(clientConnection.getScope(), "hostStream"); // Get a handle to the stream
clientStream.stopRecording(); // Stop recording the stream
}
// Need this for Spring to work right :/
public void setApplication(Application app)
{
this.app = app;
}
}
This part was pretty mindless; the source is copied almost exactly from here.
As for those .xml files, here's what mine look like:
red5-web.properties
webapp.contextPath=/as-record
webapp.virtualHosts=localhost, 127.0.0.1, 173.255.237.37
red5-web.xml
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE beans PUBLIC "-//SPRING//DTD BEAN//EN" "http://www.springframework.org/dtd/spring-beans.dtd">
<beans>
<bean id="placeholderConfig" class="org.springframework.beans.factory.config.PropertyPlaceholderConfigurer">
<property name="location" value="/WEB-INF/red5-web.properties" />
</bean>
<bean id="web.context" class="org.red5.server.Context" autowire="byType" />
<bean id="web.scope" class="org.red5.server.WebScope" init-method="register">
<property name="server" ref="red5.server" />
<property name="parent" ref="global.scope" />
<property name="context" ref="web.context" />
<property name="handler" ref="web.handler" />
<property name="contextPath" value="${webapp.contextPath}" />
<property name="virtualHosts" value="${webapp.virtualHosts}" />
</bean>
<bean id="StreamManager.service" class="net.acsurvey.StreamManager">
<property name="application" ref="web.handler"/>
</bean>
<bean id="web.handler" class="net.acsurvey.Application" />
</beans>
This file is the interesting one; it's where you define all of the services your app provides and how clients can ask for them. The critical bean here is web.scope
as it is the default handler for all incoming requests, RTMP and otherwise. The other important bean for my project is the StreamManager
one; it's responsible for saving the incoming streams to disk. Defining this as a separate service makes it a little easier to write ActionScript code to talk to it, but that's another post.
Note that org.red5.server.WebScope
becomes org.red5.server.scope.WebScope
in version 1.0 of the API.
web.xml
<?xml version="1.0" encoding="ISO-8859-1"?>
<web-app
xmlns="http://java.sun.com/xml/ns/j2ee"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://java.sun.com/xml/ns/j2ee http://java.sun.com/xml/ns/j2ee/web-app_2_4.xsd"
version="2.4">
<display-name>Accent Survey Recording App</display-name>
<context-param>
<param-name>webAppRootKey</param-name>
<param-value>/as-record</param-value>
</context-param>
</web-app>
Getting all of that working was extremely simple; I barely had to do more than copy and paste from a few places. It's useful to me to have all of this information in one place though, so here it is.
I'll probably do another post sometime in the near future that documents the ActionScript side of this application and the other server-side things I had to do to get everything working smoothly.