/* Milenia Grafter Server Copyright (c) 2007-2008 by Milan Toth. All rights reserved. This program is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License of the License, or (at your option) any later version. This program 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 General Public License for more details. You should have received a copy of the GNU General Public License along with this program; if not, write to the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ package com.milgra.server; /** StreamController class @mail milgra@milgra.com @author Milan Toth @version 20080315 Tasks of Streamcontroller - control stream creation, deletion - control play, publish request - receive and dispatch stream packets **/ import java.util.HashMap; import java.util.ArrayList; import com.milgra.server.api.Stream; import com.milgra.server.api.Wrapper; import com.milgra.server.api.WrapperList; import com.milgra.server.api.StreamEvent; public class StreamController extends OProcess { // closed - controller is closed public boolean closed; // flvId - flv channel counter for outgoing streams // streamId - stream id counter for incoming streams // channnelIndex - rtmp channel counter public int flvId; public int streamId; public int channelIndex; // hasAudio - do we received audio data // hasVideo - do we received video data public boolean hasAudio; public boolean hasVideo; // frame drop control booleans public boolean dropAudio; public boolean dropKeyframes; public boolean dropInterframes; public boolean dropDisposables; // controllers public SocketController socket; public ClientController client; // request waiting lists public HashMap < Integer , Integer > channelToId; public ArrayList < Integer > channels; public ArrayList < Integer > idSequence; public ArrayList < Stream > createRequests; // RTMP packet waiting list public ArrayList < RtmpPacket > incomingList; public ArrayList < RtmpPacket > outgoingList; // stream containers // players - standard stream players subscribing to a router // routers - stream routers related to our client // remotes - stream players pushing stream to a remote client after acception // address - stream router addresses to routers to make sorting more simple public HashMap < Integer , StreamPlayer > players; public HashMap < Integer , StreamRouter > routers; public HashMap < Integer , StreamPlayer > remotes; public HashMap < Integer , StreamRouter > address; /** * StreamController constructor * @param clientX client controller * @param socketX socket controller */ public StreamController ( ClientController clientX , SocketController socketX ) { // System.out.println( System.currentTimeMillis( ) + " " + clientX.id + " StreamController.construct" ); // create players = new HashMap < Integer , StreamPlayer > ( ); routers = new HashMap < Integer , StreamRouter > ( ); remotes = new HashMap < Integer , StreamPlayer > ( ); address = new HashMap < Integer , StreamRouter > ( ); channels = new ArrayList < Integer > ( ); idSequence = new ArrayList < Integer > ( ); channelToId = new HashMap < Integer , Integer > ( ); createRequests = new ArrayList < Stream > ( ); // set // controllers client = clientX; socket = socketX; // starting channel and id // flvchannels starting from 01 00 00 00 // streamids starting from 1 // channelIndex starts from the first flvId = 1 << 24; streamId = 1; channelIndex = 0; // frame drop controls dropAudio = false; dropKeyframes = false; dropInterframes = false; dropDisposables = false; // packet containers incomingList = new ArrayList < RtmpPacket > ( ); outgoingList = new ArrayList < RtmpPacket > ( ); // start // fill up rtmp channels for ( int index = 4 ; index < 64 ; index++ ) channels.add( index ); } /** * @return new rtmp channel for a stream **/ public int getChannel ( ) { // System.out.println( System.currentTimeMillis( ) + " " + clientX.id + " StreamController.getChannel" ); // for better header scattering, streams should vary avaliable rtmp channels int channel = channels.remove( channelIndex++ ); if ( channelIndex == channels.size( ) ) channelIndex = 0; return channel; } /** * Closes StreamController **/ public void close ( ) { // System.out.println( System.currentTimeMillis( ) + " " + client.id + " StreamController.close" ); closed = true; ArrayList < StreamPlayer > playersCopy = new ArrayList < StreamPlayer > ( players.values( ) ); ArrayList < StreamRouter > routersCopy = new ArrayList < StreamRouter > ( routers.values( ) ); for ( StreamPlayer player : playersCopy ) player.close( ); for ( StreamRouter router : routersCopy ) router.close( ); } /** * Removes stream player from registers * @param playerX stream player **/ public void removePlayer( StreamPlayer playerX ) { // System.out.println( System.currentTimeMillis( ) + " " + client.id + " StreamController.removePlayer " + playerX ); players.remove( playerX.getId( ) ); channels.add( playerX.getAudioChannel( ) ); channels.add( playerX.getVideoChannel( ) ); } /** * Removes stream router from registers * @param playerX stream router */ public void removeRouter( StreamRouter routerX ) { // System.out.println( System.currentTimeMillis( ) + " " + client.id + " StreamController.removeRouter " + routerX ); routers.remove( routerX.getId( ) ); address.remove( routerX.getFlvChannel( ) ); } /** * Creates new stream, passing back a newly generated stream id * @param argumentsX arguments * @param packetX rtmp packet */ public void onStreamCreateRequest( WrapperList argumentsX , RtmpPacket packetX ) { // System.out.println( System.currentTimeMillis( ) + " " + client.id + " StreamController.onStreamCreateRequest " + argumentsX.get( 1 ).doubleValue ); int actualId; double invokeId; // get invoke channel of request // generate new streamId invokeId = argumentsX.getDouble( 1 ); actualId = streamId++; // store id in the sequence container // call result on client idSequence.add( actualId ); client.callResult( invokeId , new Wrapper( actualId ) ); } /** * Deletes a stream by id, called by custom application or client * @param argumentsX arguments * @param packetX rtmp packet */ public void onStreamDeleteRequest( WrapperList argumentsX , RtmpPacket packetX ) { // System.out.println( System.currentTimeMillis( ) + " " + client.id + " StreamController.onStreamDeleteRequest " + argumentsX ); // get stream id from request double wantedId = argumentsX.getDouble( 3 ); // delete related stuff if ( players.containsKey( wantedId ) ) { channelToId.remove( players.get( wantedId ).getFlvChannel( ) ); players.get( wantedId ).close( ); } if ( routers.containsKey( wantedId ) ) { channelToId.remove( routers.get( wantedId ).getFlvChannel( ) ); routers.get( wantedId ).close( ); } } /** * Closes a stream * @param argumentsX arguments * @param packetX rtmp packet */ public void onStreamCloseRequest( ArrayList < Wrapper > argumentsX , RtmpPacket packetX ) { // System.out.println( System.currentTimeMillis( ) + " " + client.id + " StreamController.onStreamDeleteRequest " + argumentsX ); // creating clone lists, because request only contains the flv channel of closable stream ArrayList < StreamPlayer > playersCopy = new ArrayList < StreamPlayer > ( players.values( ) ); ArrayList < StreamRouter > routersCopy = new ArrayList < StreamRouter > ( routers.values( ) ); // searching for streams to close for ( StreamPlayer player : playersCopy ) if ( player.getFlvChannel( ) == packetX.flvChannel ) { channelToId.remove( player.getFlvChannel( ) ); player.close( ); } for ( StreamRouter router : routersCopy ) if ( router.getFlvChannel( ) == packetX.flvChannel ) { channelToId.remove( router.getFlvChannel( ) ); router.close( ); } } /** * Stream play request from client * @param argumentsX the arguments * @param packetX rtmp packet */ public void onStreamPlayRequest( WrapperList argumentsX , RtmpPacket packetX ) { // System.out.println( System.currentTimeMillis( ) + " StreamController.onStreamPlayRequest " + argumentsX.getString( 3 ) ); int id; String name = argumentsX.getString( 3 ); // close old player if ( channelToId.containsKey( packetX.flvChannel ) ) { id = channelToId.get( packetX.flvChannel ); StreamPlayer player = players.get( id ); if ( player != null ) player.close( ); } else { // get id if needed id = idSequence.remove( 0 ); } // store id to channel channelToId.put( packetX.flvChannel , id ); // create new player if ( name != null ) { int flvChannel = packetX.flvChannel; int videoChannel = getChannel( ); int audioChannel = getChannel( ); StreamPlayer player = new StreamPlayer( id , flvChannel , videoChannel , audioChannel , name , this ); players.put( id , player ); if ( client.streamListener == null ) player.enable( ); else client.streamListener.onEvent( new StreamEvent( new Stream( player ) , client.client ) ); } } /** * Stream publish request from client * @param argumentsX the arguments * @param packetX rtmp packet */ public void onStreamPublishRequest( WrapperList argumentsX , RtmpPacket packetX ) { // System.out.println( System.currentTimeMillis( ) + " StreamController.onStreamPublishRequest " + argumentsX.getString( 3 ) ); int id; String name = argumentsX.getString( 3 ); // close old router if ( channelToId.containsKey( packetX.flvChannel ) ) { id = channelToId.get( packetX.flvChannel ); StreamRouter router = routers.get( id ); router.close( ); } else { // get new id if needed id = idSequence.remove( 0 ); } // store id channelToId.put( packetX.flvChannel , id ); // create new player if ( name != null ) { int flvChannel = packetX.flvChannel; String mode = argumentsX.size( ) > 4 ? argumentsX.getString( 4 ) : "live"; StreamRouter router = new StreamRouter( id , flvChannel , name , mode , this ); routers.put( id , router ); address.put( flvChannel , router ); if ( client.streamListener == null ) router.enable( ); else { Stream stream = new Stream( router ); client.streamListener.onEvent( new StreamEvent( stream , client.client ) ); } } } /** * Remote stream create request from a remote stream instance * @param streamX stream instance */ public void createStream ( Stream streamX ) { // System.out.println( System.currentTimeMillis() + " " + client.id + " StreamController.createStream " + streamX ); createRequests.add( streamX ); client.callResponse( "createStream" , new WrapperList( ) , 0 ); } /** * Deletes a remote stream * @param streamIdX stream id * @param streamX stream instance */ public void deleteStream ( double streamIdX , Stream streamX ) { // System.out.println( System.currentTimeMillis() + " " + client.id + " StreamController.deleteStream " + streamIdX + " " + streamX ); if ( streamX.remoteType.equals( "play" ) ) stopRemote( ( int ) streamIdX ); if ( streamX.remoteType.equals( "publish" ) ) unpublishRemote( ( int ) streamIdX ); client.call( "deleteStream" , new Wrapper( streamIdX ) ); } /** * Create stream response from remote connection * @param streamIdX stream id */ public void onCreateStream ( double streamIdX ) { // System.out.println( System.currentTimeMillis() + " " + client.id + " StreamController.onCreateStream " + streamIdX ); // getting oldest waiting stream instance // setting remote id Stream stream = createRequests.remove( 0 ); stream.remoteId = ( int ) streamIdX; stream.remoteChannel = flvId--; // if stream is a remote player, create a related router for incoming stream // if stream is a remote publisher, create a player for utgoing stream boradcasting if ( stream.remoteType.equals( "play" ) ) playRemote( ( int ) streamIdX , stream.remoteChannel , stream.remoteName , stream.name ); if ( stream.remoteType.equals( "publish" ) ) publishRemote( ( int ) streamIdX , stream.remoteChannel , stream.remoteName , stream.name , stream.mode ); } /** * Creates a router for incoming stream * @param idX * @param nameX */ public void playRemote ( int idX , int channelX , String remoteNameX , String localNameX ) { // System.out.println( System.currentTimeMillis() + " " + client.id + " StreamController.playRemote " + idX + " " + remoteNameX + " " + localNameX ); // generate new id and channel int newId = streamId++; // create router StreamRouter router = new StreamRouter( newId , channelX , localNameX , "live" , this ); router.enable( ); // register router routers.put( newId , router ); address.put( channelX , router ); // send play request client.call( "play" , new WrapperList( new Wrapper( remoteNameX ) ) , channelX ); } /** * Stops a remote player * @param idX stream id */ public void stopRemote ( int idX ) { // System.out.println( System.currentTimeMillis() + " " + client.id + " StreamController.stopRemote " + idX ); // getting router StreamRouter router = routers.get( idX ); // unregister routers.remove( idX ); address.remove( router.getFlvChannel( ) ); // close here and there router.close( ); } /** * Creates a player for outgoing stream * @param idX stream id * @param nameX local name of stream * @param modeX mode of publishing */ public void publishRemote ( int idX , int channelX , String remoteNameX , String localNameX , String modeX ) { // System.out.println( System.currentTimeMillis() + " " + client.id + " StreamController.publishRemote " + idX + " " + remoteNameX + " " + localNameX ); // generate new id and channel int newId = streamId++; int videoChannel = getChannel( ); int audioChannel = getChannel( ); // create player StreamPlayer player = new StreamPlayer( newId , channelX , videoChannel , audioChannel , localNameX , this ); player.enable( ); // register player remotes.put( idX , player ); players.put( newId , player ); // send publish request WrapperList arguments = new WrapperList( ); arguments.add( remoteNameX ); arguments.add( modeX ); client.call( "publish" , arguments , channelX ); } /** * Unpublishes a remote stream * @param idX stream id */ public void unpublishRemote ( int idX ) { // System.out.println( System.currentTimeMillis() + " " + client.id + " StreamController.publishRemote " + idX ); // getting player StreamPlayer player = remotes.get( idX ); // unregister remotes.remove( idX ); players.remove( player.getId( ) ); // close here and there player.close( ); } /** * Sets audio sending state * @param argumentsX arguments * @param packetX rtmp packet */ public void onAudioReceiveState ( WrapperList argumentsX , RtmpPacket packetX ) { // System.out.println( System.currentTimeMillis() + " " + client.id + " StreamController.onAudioReceiveState " ); boolean state = argumentsX.getBoolean( 3 ); for ( StreamPlayer player : players.values( ) ) if ( player.getFlvChannel( ) == packetX.flvChannel ) player.enableAudio( state ); } /** * Sets video sending state * @param argumentsX arguments * @param packetX rtmp packet */ public void onVideoReceiveState ( WrapperList argumentsX , RtmpPacket packetX ) { // System.out.println( System.currentTimeMillis() + " " + client.id + " StreamController.onVideoReceiveState " ); boolean state = argumentsX.getBoolean( 3 ); for ( StreamPlayer player : players.values( ) ) if ( player.getFlvChannel( ) == packetX.flvChannel ) player.enableVideo( state ); } /** * Seek request from client * @param argumentsX arguments * @param packetX rtmp packet */ public void onStreamSeekRequest( WrapperList argumentsX , RtmpPacket packetX ) { // System.out.println( System.currentTimeMillis( ) + " " + client.id + // "StreamController.onStreamSeekRequest" + packetX.toString( ) + " " + argumentsX.getDouble( 3 ) ); for ( StreamPlayer player : players.values( ) ) if ( player.getFlvChannel( ) == packetX.flvChannel ) player.reader.setPosition( argumentsX.getDouble( 3 ) ); } /** * Speed request from client * @param argumentsX arguments * @param packetX rtmp packet */ public void onStreamSpeedRequest ( WrapperList argumentsX , RtmpPacket packetX ) { //System.out.println( System.currentTimeMillis( ) + " " + client.id + // "StreamController.onStreamSpeedRequest" + packetX.toString( ) + " " + argumentsX.getString( 3 )+ " " + argumentsX.getDouble( 4 ) ); for ( StreamPlayer player : players.values( ) ) if ( player.getName( ).equals( argumentsX.getString( 3 ) ) ) player.reader.setSpeed( new Double( argumentsX.getDouble( 4 ) ) ); } /** * Pause request from client * @param argumentsX arguments * @param packetX rtmp packet */ public void onStreamPauseRequest ( WrapperList argumentsX , RtmpPacket packetX ) { //System.out.println( System.currentTimeMillis( ) + " " + client.id + // "StreamController.onStreamSpeedRequest" + packetX.toString( ) + " " + argumentsX.getDouble( 3 ) ); for ( StreamPlayer player : players.values( ) ) if ( player.getFlvChannel( ) == packetX.flvChannel ) player.reader.pause( ); } /** * Sets the buffer length for the routers on the specified channel * @param idX stream id * @param bufferLengthX buffer size */ public void setBufferLength ( double streamIdX , int bufferLengthX ) { // System.out.println( System.currentTimeMillis( ) + " " + client.id + " StreamController.setBufferLength " + streamIdX + " " + bufferLengthX ); //if ( routers.containsKey( streamIdX ) ) // routers.get( streamIdX ).setBufferLength( bufferLengthX ); } /** * Steps one in packet multiplexing */ public void step ( ) { // System.out.println( System.currentTimeMillis( ) + " " + client.id + " StreamController.step ogList: " + outgoingList.size( ) ); // get incoming flv packets socket.giveFlvPackets( incomingList ); // sort them for ( RtmpPacket packet : incomingList ) if ( address.containsKey( packet.flvChannel ) ) address.get( packet.flvChannel ).take( packet ); // get sorted packets for ( StreamPlayer player : players.values( ) ) player.givePackets( outgoingList ); // give available packets socket.takePackets( outgoingList ); incomingList.clear( ); } /** * Sets dropping to a higher level */ public void addDropping ( ) { // System.out.println( System.currentTimeMillis( ) + " " + client.id + " StreamController.addDropping" ); if ( !dropDisposables ) dropDisposables = true; else if ( !dropInterframes ) dropInterframes = true; else if ( !dropKeyframes ) dropKeyframes = true; else if ( !dropAudio ) dropAudio = true; } /** * Sets dropping to a lower level */ public void removeDropping ( ) { //System.out.println( System.currentTimeMillis( ) + " " + client.id + " StreamController.removeDropping" ); if ( dropAudio ) dropAudio = false; else if ( dropKeyframes ) dropKeyframes = false; else if ( dropInterframes ) dropInterframes = false; else if ( dropDisposables ) dropDisposables = false; } /** * Returns played stream list * @return hashmap containing players */ public HashMap < Integer , Stream > getPlayers ( ) { // System.out.println( System.currentTimeMillis() + " " + client.id + " StreamController.getPlayedStreams" ); HashMap < Integer , Stream > result = new HashMap < Integer , Stream > ( ); //for ( Stream player : players.values( ) ) result.put( player.id , player ); return result; } /** * Returns published stream list * @return hashmap containing routers */ public HashMap < Integer , Stream > getRouters ( ) { //System.out.println( System.currentTimeMillis() + " " + client.id + " StreamController.getPublishedStreams " ); HashMap < Integer , Stream > result = new HashMap < Integer , Stream > ( ); //for ( Stream router : routers.values( ) ) result.put( router.id , router ); return result; } }