Hi,
before to start writing I have to say that the next information could be interesting for lot of people but I'm going to write in spanish because my programs were written in spanish and, now, I don't have enough time to translate them. But don't worry, if somebody needs to contact me and says me that is important for he/she to understand my programs I promise help him/her in all possible.
In summary, I'm going to introduce some aspects of this protocol and then I will write a brief explanation of each of my programs: one basic client/server application written in C, one basic client/server application written in Java and, finally, one client/server application written in Java and using JMF (Java Media Framework) that lets build a real-time video streaming scenario.
-----------------------------------------------------------------------------------
Comienzo con la entrada en sí.
En resumen, voy a introducir algunos aspectos de este protocolo y, después, una breve explicación de cada uno de mis programas: una aplicación básica cliente/servidor escrita en C, una aplicación básica cliente/servidor escrita en Java y una aplicación cliente/servidor escrita en Java y usando JMF (Java Media Framework) que permite construir un escenario de streaming de video en tiempo real.
INTRODUCCIÓN:
SCTP o protocolo de control de transmisión de flujo, es un protocolo de nivel de transporte orientado a conexión que, a diferencia de TCP y su concepto de conexión, introduce una nueva forma de comunicación entre sistemas denominada asociación.
Las principales características de SCTP son las siguientes:
- Es un protocolo punto a punto. Se establece el intercambio de datos entre dos extremos conocidos.
- Proporciona transporte fiable de datos de usuario, detectando y reparando los datos erróneos o fuera de secuencia.
- Se adapta a la tasa de transferencia, disminuyendo la velocidad de envío de datos en caso de congestión en la red.
- Multi-homing. Permite establecer asociaciones robustas entre dos extremos cada uno de los cuales puede ser alcanzado mediante más de una dirección de red. Hacia cada una de ellas se encaminan los mensajes de forma independiente de manera que, si una de las interfaces de red queda fuera de servicio, la comunicación no se ve afectada ya que el flujo de datos se redirige por una de las otras, si las hay.
- Cada asociación puede contener uno o mas streams que permiten el envío de datos de forma independiente entre cada uno de ellos.
Las aplicaciones ha sido probadas sobre dos máquinas virtuales creadas con VMware Server y con sistema operativo SuSE Linux 10.3. Es necesario instalar el paquete lksctp para que el SO soporte este protocolo. El hecho de haber utilizado máquinas virtuales es debido a la gran facilidad que ofrecen para crear interfaces de red, ya que va a ser necesario utilizar al menos dos interfaces de red en al menos un extremo para probar la capacidad de multihoming que ofrece SCTP.
CLIENTE/SERVIDOR EN C
La aplicación es sencilla de utilizar y nos ofrece la posibilidad de utilizar una importante funcionalidad que ofrece SCTP como novedad frente a TCP o UDP, es decir, el alta y baja de direcciones IP dentro de una asociación durante la interacción entre el cliente y el servidor y el establecimiento de la dirección primaria utilizada en cada extremo para transmitir la información.
El cliente de esta aplicación se encuentra en el fichero cliente_one.c ya que implementa sockets de tipo one_to_one. Este cliente mostrará un menú al usuario de forma que éste pueda realizar las tareas necesarias. Es el que se encarga de mandar las operaciones al servidor.
Para compilar y ejecutar dicho fichero se deberán introducir los siguientes comandos en el terminal:
- Compilar: gcc -Wall -o cliente cliente_one.c -lsctp -L/usr/local/lib
- Ejecutar: ./cliente
Al ejecutarlo pedirá al usuario la/s dirección/es y puerto del servidor a los que desea conectarse, así como la/s dirección/es y puerto desde los que desea realizar la asociación. Después de realizarse la conexión se mostrarán ciertos parámetros asociados a ella y el menú principal de opciones, a través de las cuales el usuario interactuará con la aplicación.
El servidor se encuentra en el fichero servidor_one.c implementando sockets de tipo one_to_one. Su funcionalidad se limita al tratamiento de los mensajes que le llegan del cliente, mostrando por pantalla cada
evento producido y los cambios en su configuración.
Para compilar y ejecutar la aplicación servidor se deberán introducir los siguientes comandos en el terminal:
evento producido y los cambios en su configuración.
Para compilar y ejecutar la aplicación servidor se deberán introducir los siguientes comandos en el terminal:
- Compilar: gcc -Wall -o servidor servidor_one.c -lsctp -L/usr/local/lib
- Ejecutar: ./servidor
Los programas se pueden descargar desde aquí: cliente , servidor y headers
CLIENTE/SERVIDOR EN JAVA
Para realizar esta aplicación se ha utilizado un API de sockets al que se puede acceder desde aquí. Este API no se encuentra completo por lo que hay parte de funcionalidad de SCTP que no se podrá probar con esta aplicación, pero ha sido importante familiarizarse con esta parte para poder realizar la aplicación de streaming de vide, ya que está escrita en Java y utiliza este API. Creo que Sun Microsystems está creando un API mas completo para el que desee pegarse un poco con él.
El cliente se encuentra implementado en el fichero ClienteOne.java. El nombre se debe a que el tipo de socket utilizado es de tipo one-to-one.
- Compilación: javac ClienteOne.java
- Ejecución: java -cp . -Djava.library.path=. -Xcheck:jni ClienteOne
El servidor se ha desarrollado en el fichero ServidorOne.java.
- Compilación: javac ServidorOne.java
- Ejecución: java -cp . -Djava.library.path=. -Xcheck:jni ServidorOne
STREAMING DE VIDEO MEDIANTE SCTP/JMF
Las dos aplicaciones anteriores nos sirven para tener una primera toma de contacto con el uso de las funciones que permiten utilizar las características del protocolo SCTP. Nuestro objetivo ahora, es utilizar estos conocimientos para realizar streaming de video sobre SCTP, para lo cual podemos utilizar JMF. Java Media Framework es un API que proporciona herramientas para la captura, procesamiento, almacenamiento y reproducción de datos multimedia.
¿Por qué utilizar SCTP como protocolo de transporte para el intercambio de datos en tiempo real? La razón que más me interesa destacar es que permite tener uno o más interfaces de red que se pueden utilizar como backup para que no haya cortes en la transmisión en caso de caída de los interfaces principales.
¿Por qué utilizar JMF para crear una aplicación cliente/servidor multimedia? JMF ofrece todo lo necesario para implementar una aplicación multimedia y, además, nos permite sustituir el protocolo de transporte utilizado. En este caso, se ha utilizado SCTP en vez de TCP o UDP.
He conseguido realizar una aplicación cliente/servidor mediante el API que ofrece Java Media Framework que utiliza RTP para realizar el streaming de datos, RTCP para llevar la información de control del flujo RTP y SCTP como nivel de transporte. Al menos de momento, no puedo colgar esta aplicación porque tengo la intención de utilizarla para construir un sistema de videovigilancia mediante cámaras IP.
En cualquier caso, no es demasiado costoso hacer una aplicación de este tipo, pero hay que tener siempre presente y muy claro el orden en la pila de protocolos para todos los pasos que hay que realizar. Daré una serie de pautas que espero os sirvan para crear vuestra aplicación.
Arquitectura y funcionamiento
El concepto de modelo de procesamiento de datos que sigue JMF es bastante sencillo de comprender. Como muestra la primera figura, consiste básicamente en una entrada de datos, que puede provenir de un dispositivo capturador de voz, como un micrófono; un fichero de datos, por ejemplo un videoclip de música almacenado en nuestro PC o directamente de la red, de un servidor de streaming de video, por ejemplo.
Una vez se tiene el input instanciado, se llevará a cabo un procesamiento del mismo. Este punto puede ser más o menos problemático o complejo dependiendo de las necesidades de la aplicación que se esté realizando y de las peculiaridades de la entrada y salida que se quiera gestionar/ofrecer.
Por último, los datos podrán ser presentados por pantalla mediante un interfaz gráfico, almacenados en un fichero o enviados a través de la red. El modelo fundamental es el siguiente:
Para llevar a cabo cada uno de los apartados que presenta este esquema, el API ofrece una serie de clases e interfaces que permiten al programador realizar una aplicación a medida, respondiendo a sus necesidades.
El modelo se puede repetir en el conjunto de nuestra aplicación, ya que, por ejemplo, se puede realizar un servidor que capture los datos de entrada de un micrófono, los procese y envíe a través de la red hacia un cliente que capture dicha información, realice su procesamiento y la muestre mediante un reproductor con sus correspondientes controles. En este caso, la comunicación entre cliente y servidor se realizaría mediante
RTP (Real-Time Transport Protocol) atendiendo a la estructura siguiente:
Se recomienda leer el API Guide accesible desde aquí ya que contiene toda la información necesaria acerca de JMF y múltiples ejemplos que ayudarán al lector a comprender mucho mejor lo leido.
Clases e interfaces
En este apartado se explicarán las clases e interfaces principales que ofrece JMF para tratar cada una de las partes diferenciadas en el primer esquema. El API de JMF 2.0 se puede ver aquí.
1) Entrada
Como ya se ha introducido, JMF permite capturar datos de fuentes distintas de información, por lo que, dependiendo de cada una de ellas, se deberán utilizar unas clases u otras del API.
Si el origen de estos datos es un micrófono o una cámara de video, se deberá utilizar primero la clase CaptureDeviceManager que ofrece métodos para gestionar los dispositivos del sistema utilizando mecanismos de registro y petición para localizar dichos dispositivos devolviendo objetos CaptureDeviceInfo para los que estén disponibles. El interfaz relacionado se llama DeviceManager.
CaptureDeviceInfo devideInfo = CaptureDeviceManager.getDevice(“deviceName”);
MediaLocator loc = deviceInfo.getLocator();
La clase CaptureDeviceInfo contiene un método getLocator que permite obtener un MediaLocator. Una vez obtenido el locator para este caso, los tres orígenes de datos se tratan del mismo modo. En los dos casos restantes, desde fichero y desde la red, el MediaLocator se puede construir directamente, indicando la ruta absoluta a dicho fichero, en el primer caso, o la URL, en el segundo.
MediaLocator loc = new MediaLocator(“file:///v.mpg”);
MediaLocator loc = new MediaLocator(“http://ip/v.mpg”);
La finalidad de haber obtenido el locator es poder crear un objeto de la clase DataSource que, como su propio nombre indica, será el origen de datos para la aplicación, ya que las clases de gestión, procesamiento y reproducción pueden tratar con este objeto directamente.
DataSource source = new DataSource(loc);
2) Procesamiento
Hay muchos elementos a tener en cuenta en esta parte y, dependiendo de qué tipo de aplicación se desee realizar, se utilizarán unos u otros, de forma que en este apartado no se podrá explicar de forma lineal respecto a la forma de programar. Una enumeración de las clases/interfaces mas utilizados en la “capa” de procesamiento serían Manager, Format, TrackControl, RTPManager y Processor.
La clase Manager se utiliza básicamente para crear objetos de las clases principales, es decir, DataSource, DataSink, Processor y Player, mediante sus métodos estáticos create.
DataSource dso = Manager.createDataSource(loc);
DataSink dsi = Manager.createDataSink(dso, locDest);
Processor pr = Manager.createProcessor(dso);
Player pl = Manager.createPlayer(dso);
La clase Format y, más en concreto, sus subclases AudioFormat y VideoFormat permiten gestionar el formato en que la información multimedia será procesada. El interfaz TrackControl permite establecer el formato de cada uno de los tracks que conformen el DataSource con el que se está trabajando. Para ello utiliza el método setFormat al que se le pasará una instancia de AudioFormat o VideoFormat con el
formato adecuado.
La clase abstracta RTPManager es el punto de partida para crear, gestionar y cerrar sesiones RTP. Se utilizará para crear las sesiones RTP (initialize) y los streams de salida (createSendStream), así como los listeners correspondientes para gestionar tanto streams como sesiones (addSendStreamListener, addSessionListener), añadir los destinos de la transmisión (addTarget) y finalizar todos los recursos utilizados durante el transcurso de la sesión (dispose).
RTPManager rtpManager = RTPManager.newInstance();
rtpManager.initialize(localAddress);
rtpManager.addTarget(remoteAddress);
rtpManager.createSendStream( dso, 1);
rtpManager.dispose();
El interfaz Processor, aunque extiende el interfaz Player que procesa los datos como una caja negra y sólo los envía a los destinos preseleccionados (motivo por el cual no se encuentra en esta sección), soporta un interfaz programable que permite habilitar el control sobre el procesamiento de los datos y el acceso a los streams de salida. El uso común de un Processor es el siguiente:
· demultiplexar el stream multimedia en tracks separados: el método getTrackControls devuelve un array de objetos TrackControl.
· transcodificar estos tracks pasándolos de un formato a otro: método setFormat sobre cada objeto del array.
· multiplexar los tracks separados para formar un stream entrelazado de un tipo de contenido particular: setContentDescriptor.
TrackControl [] tracks = processor.getTrackControls();
tracks[i].setFormat(new VideoFormat(VideoFormat.MPEG_RTP));
ContentDescriptor cd = new ContentDescriptor(ContentDescriptor.RAW_RTP);
processor.setContentDescriptor(cd);
El Processor tiene un ciclo de vida que habrá que tener en cuenta a la hora de llamar a sus métodos ya que, dependiendo del estado en que se encuentre, se podrán invocar unos u otros.
Cuando se llama al método configure del Processor, éste pasa a estado “Configuring” en el que se conecta al DataSource, demultiplexa el stream de entrada y accede a la información sobre el formato de los datos. Pasará a estado “Configured” cuando se ha realizado la conexión al DataSource y se ha determinado el formato de los datos. En tal caso, será enviado el evento ConfigureCompleteEvent. Tras llamar al método realize de Processor pasará a estado “Realized” en el que se considerará totalmente construido.
3) Salida
De la misma forma que sucede con la entrada de datos, la salida también se puede realizar de distintas formas: se puede presentar mediante un reproductor multimedia utilizando el interfaz Player, se puede almacenar en un fichero mediante la clase DataSource o se puede enviar a través de la red utilizando DataSink o RTPManager.
JMF posee un interfaz denominado Player que, como su propio nombre indica, permite realizar un reproductor multimedia ya que ofrece métodos para obtener componentes AWT, controles para el procesamiento de los datos y formas de gestionar otros Controllers.
class PlayerPanel extends Panel {
Component vc, cc;
PlayerPanel() {
Player p = Manager.createPlayer(dso);
setLayout(new BorderLayout());
vc = p.getVisualComponent();
add("Center", vc);
cc = p.getControlPanelComponent();
add("South", cc);
}
}
Al igual que sucede con el Processor, el Player también tiene un ciclo de vida que hay que respetar.
Cuando creamos una instancia de un Player, éste se encuentra en estado “Unrealized”. La llamada a su método realize pone al Player en estado “Realizing”, en el cual, se determinan los requerimientos de los recursos necesarios para operar. Al terminar esta fase, se pasa al estado “Realized” en el que el Player conoce los recursos necesarios que necesita y la información sobre el tipo de información que va a
presentar. Por ello, en este momento puede ofrecer componentes visuales y de control. El siguiente paso es llamar al método prefetch, de forma que se alcanza el estado “Prefetching” en el que el Player está preparándose para presentar la información multimedia, precargando los datos, obteniendo recursos de uso exclusivo y haciendo lo que necesite para prepararse para reproducir. Al finalizar quedará en estado “Prefetched” en el que ya se puede llamar al método start para iniciar la reproducción. La llamada a este método pondrá al Player en estado “Started”.
Dentro de la aplicación, el programador puede ir capturando los eventos de transición entre estados enviados por el Player. El interfaz ControllerListener ofrece una forma hacerlo mediante la implementación de su método controllerUpdate dentro del cual se puede ir diferenciando entre los distintos eventos del interfaz Controller realizando el tratamiento adecuado según el caso.
La segunda opción de tratamiento de datos a la salida y la más sencilla, es el almacenamiento de datos en un fichero de destino. Tras utilizar un Processor para realizar algún tipo de procesamiento sobre el DataSource de entrada, se puede obtener el DataSource de salida que será almacenado en un fichero de la siguiente forma:
DataSource dataOutput = processor.getDataOutput();
MediaLocator dest = new MediaLocator(“file:///out.mpg”);
DataSink sink = Manager.createDataSink(dataOutput, dest);
sink.open();
sink.start();
Por último, se pueden enviar datos a través de la red mediante RTP de dos formas:
· Utilizando un MediaLocator que contendrá los parámetros de una sesión RTP para construir un DataSink.
· Utilizando un RTPManager para crear streams de salida y controlar la transmisión.
Si se utiliza la primera opción sólo se podrá transmitir el primer stream del DataSource, por lo que, si se desea realizar una aplicación más compleja en la que se necesiten recibir múltiples streams, monitorizar las estadísticas de la sesión o realizar cierto tipo de procesamiento basado en la recepción de eventos, se debe utilizar RTPManager.
El interfaz DataSink permite leer datos de un DataSource y enviarlos hacia algún destino que será especificado mediante un MediaLocator. Estas instancias serán indicadas como parámetros para construir el DataSink:
String url = “rtp://124.124.2.4:4242/audio/1”;
MediaLocator locDest = new MediaLocator(url);
DataSink dsi = Manager.createDataSink(dso, locDest);
Como se indicó en el apartado de procesamiento, la clase RTPManager es el núcleo para realizar aplicaciones RTP complejas. Esta clase ofrece dos formas de establecer sesiones RTP para la transmisión de datos multimedia:
· Utilizando los métodos addTarget y removeTarget, de forma que el nivel de transporte es UDP.
· Utilizar una clase que implemente el interfaz RTPConnector en la que el programador implemente el nivel de transporte que desee.
Para usar una u otra forma, RTPManager posee tres métodos initialize, dos para el primer caso y uno para el segundo. Por otra parte, siempre que el nivel de transporte sea UDP, las transmisiones podrán ser multicast además de unicast.
RTPConnector es un interfaz que implementa la capa de transporte subyacente bajo RTP. El programador deberá realizar una clase que implemente esta interfaz para ser utilizada por RTPManager realizando la gestión de la recepción de paquetes de datos (RTP) y de control (RTCP- Real Time Control Protocol).
RTPManager rtpManager = RTPManager.newInstance();
int port = 20000;
//Se crea una instancia de la clase que implementa el interfaz RTPConnector.
//En este caso la capa de transporte es SCTP, por lo que primero se debe establecer la asociación
RTPSocketHandler handler = new RTPSocketHandler(port);
handler.listenSocket();
handler.connectSocket(“127.0.0.1”, port);
//Inicializa RTPManager
rtpMgrs[i].initialize(handler);
SendStream sendStream = rtpManager.createSendStream(dso, 1);
sendStream.start();
Esta es una captura de los players generados por mi aplicación:
Desde estas ventanas se puede controlar el video y audio, así como ver las características del flujo de datos que es está recibiendo y el desempaquetado que se está llevando a cabo.
En la parte del servidor he hecho que se mostrará la siguiente ventana que mostrará la información de los tracks contenidos en el fichero multimedia y un botón close para terminar la transmisión cuando se desee.
Un saludo.