package org.red5.server.adapter;
/*
* RED5 Open Source Flash Server - http://www.osflash.org/red5
*
* Copyright (c) 2006-2009 by respective authors (see below). All rights reserved.
*
* This library 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 2.1 of the License, or (at your option) any later
* version.
*
* This library 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 this library; if not, write to the Free Software Foundation, Inc.,
* 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
*/
import static org.red5.server.api.ScopeUtils.getScopeService;
import static org.red5.server.api.ScopeUtils.isApp;
import static org.red5.server.api.ScopeUtils.isRoom;
import java.io.File;
import java.io.IOException;
import java.util.Collection;
import java.util.Collections;
import java.util.Date;
import java.util.HashSet;
import java.util.List;
import java.util.Set;
import org.red5.io.IStreamableFile;
import org.red5.io.IStreamableFileFactory;
import org.red5.io.IStreamableFileService;
import org.red5.io.ITagReader;
import org.red5.io.StreamableFileFactory;
import org.red5.logging.Red5LoggerFactory;
import org.red5.server.api.IClient;
import org.red5.server.api.IConnection;
import org.red5.server.api.IScope;
import org.red5.server.api.Red5;
import org.red5.server.api.ScopeUtils;
import org.red5.server.api.scheduling.IScheduledJob;
import org.red5.server.api.scheduling.ISchedulingService;
import org.red5.server.api.service.ServiceUtils;
import org.red5.server.api.so.ISharedObject;
import org.red5.server.api.so.ISharedObjectSecurity;
import org.red5.server.api.so.ISharedObjectSecurityService;
import org.red5.server.api.so.ISharedObjectService;
import org.red5.server.api.stream.IBroadcastStream;
import org.red5.server.api.stream.IBroadcastStreamService;
import org.red5.server.api.stream.IClientBroadcastStream;
import org.red5.server.api.stream.IOnDemandStream;
import org.red5.server.api.stream.IOnDemandStreamService;
import org.red5.server.api.stream.IPlayItem;
import org.red5.server.api.stream.IPlaylistSubscriberStream;
import org.red5.server.api.stream.IStreamAwareScopeHandler;
import org.red5.server.api.stream.IStreamPlaybackSecurity;
import org.red5.server.api.stream.IStreamPublishSecurity;
import org.red5.server.api.stream.IStreamSecurityService;
import org.red5.server.api.stream.IStreamService;
import org.red5.server.api.stream.ISubscriberStream;
import org.red5.server.api.stream.ISubscriberStreamService;
import org.red5.server.exception.ClientRejectedException;
import org.red5.server.scheduling.QuartzSchedulingService;
import org.red5.server.so.SharedObjectService;
import org.red5.server.stream.ClientBroadcastStream;
import org.red5.server.stream.IBroadcastScope;
import org.red5.server.stream.IProviderService;
import org.red5.server.stream.PlaylistSubscriberStream;
import org.red5.server.stream.ProviderService;
import org.red5.server.stream.StreamService;
import org.slf4j.Logger;
/**
* ApplicationAdapter class serves as a base class for your Red5 applications.
* It provides methods to work with SharedObjects and streams, as well as
* connections and scheduling services.
*
* ApplicationAdapter is an application level IScope. To handle streaming
* processes in your application you should implement
* {@link IStreamAwareScopeHandler} interface and implement handling methods.
*
* Application adapter provides you with useful event handlers that can be used to intercept streams,
* authorize users, etc. Also, all methods added in subclasses can be called from client side with NetConnection.call
* method. Unlike to Flash Media server which requires you to keep methods on Client object at server side, Red5
* offers much more convenient way to add methods for remote invocation to your applications.
*
* <p><strong>EXAMPLE:</strong></p>
* <p>
* <code>
* public List<String> getLiveStreams() {<br />
* // Implementation goes here, say, use Red5 object to obtain scope and all it's streams<br />
* }<br />
* </code>
*
* <p>This method added to ApplicationAdapter sublass can be called from client side with the following code:</p>
*
* <code>
* var nc:NetConnection = new NetConnection();<br />
* nc.connect(...);<br />
* nc.call("getLiveStreams", resultHandlerObj);<br />
* </code>
*
* <p>If you want to build a server-side framework this is a place to start and wrap it around ApplicationAdapter subclass.</p>
* </p>
*
* @author The Red5 Project (red5@osflash.org)
* @author Joachim Bauch (jojo@struktur.de)
* @author Paul Gregoire (mondain@gmail.com)
* @author Michael Klishin
*/
public class MultiThreadedApplicationAdapter extends StatefulScopeWrappingAdapter implements
ISharedObjectService, IBroadcastStreamService, IOnDemandStreamService,
ISubscriberStreamService, ISchedulingService, IStreamSecurityService,
ISharedObjectSecurityService, IStreamAwareScopeHandler,
ApplicationMBean {
/**
* Logger object
*/
protected static Logger log = Red5LoggerFactory.getLogger(MultiThreadedApplicationAdapter.class);
/**
* List of application listeners.
*/
private Set<IApplication> listeners = new HashSet<IApplication>();
/**
* Scheduling service. Uses Quartz. Adds and removes scheduled jobs.
*/
protected ISchedulingService schedulingService;
/**
* Client time to live is max allowed ping return time, in seconds
*/
private int clientTTL = 2;
/**
* Ghost connections (disconnected users listed as connected) cleanup period in seconds
*/
private int ghostConnsCleanupPeriod = 5;
/**
* Ghost connections cleanup job name. Needed to cancel this job.
*/
private String ghostCleanupJobName;
/**
* List of handlers that protect stream publishing.
*/
private Set<IStreamPublishSecurity> publishSecurity = new HashSet<IStreamPublishSecurity>();
/**
* List of handlers that protect stream playback.
*/
private Set<IStreamPlaybackSecurity> playbackSecurity = new HashSet<IStreamPlaybackSecurity>();
/**
* List of handlers that protect shared objects.
*/
private Set<ISharedObjectSecurity> sharedObjectSecurity = new HashSet<ISharedObjectSecurity>();
/**
* Register listener that will get notified about application events. Please
* note that return values (e.g. from {@link IApplication#appStart(IScope)})
* will be ignored for listeners.
*
* @param listener object to register
*/
public void addListener(IApplication listener) {
listeners.add(listener);
}
/**
* Unregister handler that will not get notified about application events
* any longer.
*
* @param listener object to unregister
*/
public void removeListener(IApplication listener) {
listeners.remove(listener);
}
/**
* Return handlers that get notified about application events.
*
* @return list of handlers
*/
public Set<IApplication> getListeners() {
return Collections.unmodifiableSet(listeners);
}
/** {@inheritDoc} */
public void registerStreamPublishSecurity(IStreamPublishSecurity handler) {
publishSecurity.add(handler);
}
/** {@inheritDoc} */
public void unregisterStreamPublishSecurity(IStreamPublishSecurity handler) {
publishSecurity.remove(handler);
}
/** {@inheritDoc} */
public Set<IStreamPublishSecurity> getStreamPublishSecurity() {
return publishSecurity;
}
/** {@inheritDoc} */
public void registerStreamPlaybackSecurity(IStreamPlaybackSecurity handler) {
playbackSecurity.add(handler);
}
/** {@inheritDoc} */
public void unregisterStreamPlaybackSecurity(IStreamPlaybackSecurity handler) {
playbackSecurity.remove(handler);
}
/** {@inheritDoc} */
public Set<IStreamPlaybackSecurity> getStreamPlaybackSecurity() {
return playbackSecurity;
}
/** {@inheritDoc} */
public void registerSharedObjectSecurity(ISharedObjectSecurity handler) {
sharedObjectSecurity.add(handler);
}
/** {@inheritDoc} */
public void unregisterSharedObjectSecurity(ISharedObjectSecurity handler) {
sharedObjectSecurity.remove(handler);
}
/** {@inheritDoc} */
public Set<ISharedObjectSecurity> getSharedObjectSecurity() {
return sharedObjectSecurity;
}
/**
* Reject the currently connecting client without a special error message.
* This method throws {@link ClientRejectedException} exception.
*
* @return never returns
* @throws org.red5.server.exception.ClientRejectedException
* Thrown when client connection must be rejected by application logic
*/
protected boolean rejectClient() throws ClientRejectedException {
throw new ClientRejectedException();
}
/**
* Reject the currently connecting client with an error message.
*
* The passed object will be available as "application" property of the
* information object that is returned to the caller.
*
* @param reason Additional error message to return to client-side Flex/Flash application
* @return never returns
*
* @throws org.red5.server.exception.ClientRejectedException
* Thrown when client connection must be rejected by application logic
*/
protected boolean rejectClient(Object reason) throws ClientRejectedException{
throw new ClientRejectedException(reason);
}
/**
* Returns connection result for given scope and parameters. Whether the
* scope is room or app level scope, this method distinguishes it and acts
* accordingly. You override
* {@link ApplicationAdapter#appConnect(IConnection, Object[])} or
* {@link ApplicationAdapter#roomConnect(IConnection, Object[])} in your
* application to make it act the way you want.
*
* @param conn Connection object
* @param scope Scope
* @param params List of params passed to connection handler
* @return <code>true</code> if connect is successful, <code>false</code>
* otherwise
*/
@Override
public boolean connect(IConnection conn, IScope scope, Object[] params) {
if (!super.connect(conn, scope, params)) {
return false;
}
if (log.isInfoEnabled()) {
//log w3c connect event
IClient client = conn.getClient();
if (client == null) {
log.info("W3C x-category:session x-event:connect c-ip:{}", conn.getRemoteAddress());
} else {
log.info("W3C x-category:session x-event:connect c-ip:{} c-client-id:{}", conn.getRemoteAddress(), client.getId());
}
}
boolean success = false;
if (isApp(scope)) {
success = appConnect(conn, params);
} else if (isRoom(scope)) {
success = roomConnect(conn, params);
}
return success;
}
/**
* Starts scope. Scope can be both application or room level.
*
* @param scope Scope object
* @return <code>true</code> if scope can be started, <code>false</code>
* otherwise. See {@link AbstractScopeAdapter#start(IScope)} for
* details.
*/
@Override
public boolean start(IScope scope) {
if (!super.start(scope)) {
return false;
}
if (isApp(scope)) {
return appStart(scope);
} else {
return isRoom(scope) && roomStart(scope);
}
}
/**
* Returns disconnection result for given scope and parameters. Whether the
* scope is room or app level scope, this method distinguishes it and acts
* accordingly.
*
* @param conn Connection object
* @param scope Scope
*/
@Override
public void disconnect(IConnection conn, IScope scope) {
log.debug("disconnect: {} << {}", conn, scope);
if (log.isInfoEnabled()) {
//log w3c connect event
IClient client = conn.getClient();
if (client == null) {
//log w3c connect event
log.info("W3C x-category:session x-event:disconnect c-ip:{}", conn.getRemoteAddress());
} else {
//log w3c connect event
log.info("W3C x-category:session x-event:disconnect c-ip:{} c-client-id:{}", conn.getRemoteAddress(), client.getId());
}
}
if (isApp(scope)) {
appDisconnect(conn);
} else if (isRoom(scope)) {
roomDisconnect(conn);
}
super.disconnect(conn, scope);
}
/**
* Stops scope handling (that is, stops application if given scope is app
* level scope and stops room handling if given scope has lower scope
* level). This method calls {@link ApplicationAdapter#appStop(IScope)} or
* {@link ApplicationAdapter#roomStop(IScope)} handlers respectively.
*
* @param scope Scope to stop
*/
@Override
public void stop(IScope scope) {
if (isApp(scope)) {
appStop(scope);
} else if (isRoom(scope)) {
roomStop(scope);
}
super.stop(scope);
}
/**
* Adds client to scope. Scope can be both application or room. Can be
* applied to both application scope and scopes of lower level. This method
* calls {@link ApplicationAdapter#appJoin(IClient, IScope)} or
* {@link ApplicationAdapter#roomJoin(IClient, IScope)} handlers
* respectively.
*
* @param client Client object
* @param scope Scope object
*/
@Override
public boolean join(IClient client, IScope scope) {
if (!super.join(client, scope)) {
return false;
}
if (isApp(scope)) {
return appJoin(client, scope);
} else {
return isRoom(scope) && roomJoin(client, scope);
}
}
/**
* Disconnects client from scope. Can be applied to both application scope
* and scopes of lower level. This method calls
* {@link ApplicationAdapter#appLeave(IClient, IScope)} or
* {@link ApplicationAdapter#roomLeave(IClient, IScope)} handlers
* respectively.
*
* @param client Client object
* @param scope Scope object
*/
@Override
public void leave(IClient client, IScope scope) {
log.debug("leave: {} << {}", client, scope);
if (isApp(scope)) {
appLeave(client, scope);
} else if (isRoom(scope)) {
roomLeave(client, scope);
}
super.leave(client, scope);
}
/**
* Called once on scope (that is, application or application room) start.
* You override {@link ApplicationAdapter#appStart(IScope)} or
* {@link ApplicationAdapter#roomStart(IScope)}} in your application to
* make it act the way you want.
*
* @param app Application scope object
* @return <code>true</code> if scope can be started, <code>false</code>
* otherwise
*/
public boolean appStart(IScope app) {
log.debug("appStart: {}", app);
for (IApplication listener : listeners) {
listener.appStart(app);
}
return true;
}
/**
* Handler method. Called when application is stopped.
*
* @param app Scope object
*/
public void appStop(IScope app) {
log.debug("appStop: {}", app);
for (IApplication listener : listeners) {
listener.appStop(app);
}
}
/**
* Handler method. Called when room scope is started.
*
* @param room Room scope
* @return Boolean value
*/
public boolean roomStart(IScope room) {
log.debug("roomStart: {}", room);
// TODO : Get to know what does roomStart return mean
for (IApplication listener : listeners) {
listener.roomStart(room);
}
return true;
}
/**
* Handler method. Called when room scope is stopped.
*
* @param room Room scope.
*/
public void roomStop(IScope room) {
log.debug("roomStop: {}", room);
for (IApplication listener : listeners) {
listener.roomStop(room);
}
}
/**
* Handler method. Called every time new client connects (that is, new
* IConnection object is created after call from a SWF movie) to the
* application.
*
* You override this method to pass additional data from client to server
* application using <code>NetConnection.connect</code> method.
*
* <p><strong>EXAMPLE:</strong><br />
* In this simple example we pass user's skin of choice identifier from
* client to th server.</p>
*
* <p><strong>Client-side:</strong><br />
* <code>NetConnection.connect("rtmp://localhost/killerred5app", "silver");</code></p>
*
* <p><strong>Server-side:</strong><br />
* <code>if (params.length > 0) System.out.println("Theme selected: " + params[0]);</code></p>
*
* @param conn Connection object
* @param params List of parameters after connection URL passed to
* <code>NetConnection.connect</code> method.
* @return Boolean value
*/
public boolean appConnect(IConnection conn, Object[] params) {
log.debug("appConnect: {}", conn);
for (IApplication listener : listeners) {
listener.appConnect(conn, params);
}
return true;
}
/**
* Handler method. Called every time new client connects (that is, new
* IConnection object is created after call from a SWF movie) to the
* application.
*
* You override this method to pass additional data from client to server
* application using <code>NetConnection.connect</code> method.
*
* See {@link ApplicationAdapter#appConnect(IConnection, Object[])} for code
* example.
*
* @param conn Connection object
* @param params List of params passed to room scope
* @return Boolean value
*/
public boolean roomConnect(IConnection conn, Object[] params) {
log.debug("roomConnect: {}", conn);
for (IApplication listener : listeners) {
listener.roomConnect(conn, params);
}
return true;
}
/**
* Handler method. Called every time client disconnects from the
* application.
*
* @param conn Disconnected connection object
*/
public void appDisconnect(IConnection conn) {
log.debug("appDisconnect: {}", conn);
for (IApplication listener : listeners) {
listener.appDisconnect(conn);
}
}
/**
* Handler method. Called every time client disconnects from the room.
*
* @param conn Disconnected connection object
*/
public void roomDisconnect(IConnection conn) {
log.debug("roomDisconnect: {}", conn);
for (IApplication listener : listeners) {
listener.roomDisconnect(conn);
}
}
public boolean appJoin(IClient client, IScope app) {
log.debug("appJoin: {} >> {}", client, app);
for (IApplication listener : listeners) {
listener.appJoin(client, app);
}
return true;
}
/**
* Handler method. Called every time client leaves application scope.
*
* @param client Client object that left
* @param app Application scope
*/
public void appLeave(IClient client, IScope app) {
log.debug("appLeave: {} << {}", client, app);
for (IApplication listener : listeners) {
listener.appLeave(client, app);
}
}
public boolean roomJoin(IClient client, IScope room) {
log.debug("roomJoin: {} >> {}", client, room);
for (IApplication listener : listeners) {
listener.roomJoin(client, room);
}
return true;
}
/**
* Handler method. Called every time client leaves room scope.
*
* @param client Disconnected client object
* @param room Room scope
*/
public void roomLeave(IClient client, IScope room) {
log.debug("roomLeave: {} << {}", client, room);
for (IApplication listener : listeners) {
listener.roomLeave(client, room);
}
}
/**
* Try to measure bandwidth of current connection.
*
* This is required for some FLV player to work because they require the
* "onBWDone" method to be called on the connection.
*/
public void measureBandwidth() {
measureBandwidth(Red5.getConnectionLocal());
}
/**
* Try to measure bandwidth of given connection.
*
* This is required for some FLV player to work because they require the
* "onBWDone" method to be called on the connection.
*
* @param conn the connection to measure the bandwidth for
*/
public void measureBandwidth(IConnection conn) {
// dummy for now, this makes flv player work
// they dont wait for connected status they wait for onBWDone
ServiceUtils.invokeOnConnection(conn, "onBWDone", new Object[] {});
}
/* Wrapper around ISharedObjectService */
/**
* Creates a new shared object for given scope. Server-side shared objects
* (also known as Remote SO) are special kind of objects those variable are
* synchronized between clients. To get an instance of RSO at client-side,
* use <code>SharedObject.getRemote()</code>.
*
* SharedObjects can be persistent and transient. Persistent RSO are
* statuful, i.e. store their data between sessions. If you need to store
* some data on server while clients go back and forth use persistent SO
* (just use <code>true</code> ), otherwise perfer usage of transient for
* extra performance.
*
* @param scope Scope that shared object belongs to
* @param name Name of SharedObject
* @param persistent Whether SharedObject instance should be persistent or not
* @return <code>true</code> if SO was created, <code>false</code> otherwise
*/
public boolean createSharedObject(IScope scope, String name,
boolean persistent) {
ISharedObjectService service = (ISharedObjectService) getScopeService(
scope, ISharedObjectService.class, SharedObjectService.class,
false);
return service.createSharedObject(scope, name, persistent);
}
/**
* Returns shared object from given scope by name.
*
* @param scope Scope that shared object belongs to
* @param name Name of SharedObject
* @return Shared object instance with name given
*/
public ISharedObject getSharedObject(IScope scope, String name) {
ISharedObjectService service = (ISharedObjectService) getScopeService(
scope, ISharedObjectService.class, SharedObjectService.class,
false);
return service.getSharedObject(scope, name);
}
/**
* Returns shared object from given scope by name.
*
* @param scope Scope that shared object belongs to
* @param name Name of SharedObject
* @param persistent Whether SharedObject instance should be persistent or not
* @return Shared object instance with name given
*/
public ISharedObject getSharedObject(IScope scope, String name,
boolean persistent) {
ISharedObjectService service = (ISharedObjectService) getScopeService(
scope, ISharedObjectService.class, SharedObjectService.class,
false);
return service.getSharedObject(scope, name, persistent);
}
/**
* Returns available SharedObject names as List
*
* @param scope Scope that SO belong to
*/
public Set<String> getSharedObjectNames(IScope scope) {
ISharedObjectService service = (ISharedObjectService) getScopeService(
scope, ISharedObjectService.class, SharedObjectService.class,
false);
return service.getSharedObjectNames(scope);
}
/**
* Checks whether there's a SO with given scope and name
*
* @param scope Scope that SO belong to
* @param name Name of SharedObject
*/
public boolean hasSharedObject(IScope scope, String name) {
ISharedObjectService service = (ISharedObjectService) getScopeService(
scope, ISharedObjectService.class, SharedObjectService.class,
false);
return service.hasSharedObject(scope, name);
}
/* Wrapper around the stream interfaces */
/** {@inheritDoc} */
public boolean hasBroadcastStream(IScope scope, String name) {
IProviderService service = (IProviderService) getScopeService(scope,
IProviderService.class, ProviderService.class);
return (service.getLiveProviderInput(scope, name, false) != null);
}
/** {@inheritDoc} */
public IBroadcastStream getBroadcastStream(IScope scope, String name) {
IStreamService service = (IStreamService) getScopeService(scope,
IStreamService.class, StreamService.class);
if (!(service instanceof StreamService)) {
return null;
}
IBroadcastScope bs = ((StreamService) service).getBroadcastScope(scope,
name);
return (IClientBroadcastStream) bs
.getAttribute(IBroadcastScope.STREAM_ATTRIBUTE);
}
/**
* Returns list of stream names broadcasted in <pre>scope</pre>. Broadcast
* stream name is somewhat different from server stream name. Server
* stream name is just an ID assigned by Red5 to every created stream.
* Broadcast stream name is the name that is being used to subscribe to the
* stream at client side, that is, in <code>NetStream.play</code> call.
*
* @param scope Scope to retrieve broadcasted stream names
* @return List of broadcasted stream names.
*/
public List<String> getBroadcastStreamNames(IScope scope) {
IProviderService service = (IProviderService) getScopeService(scope,
IProviderService.class, ProviderService.class);
return service.getBroadcastStreamNames(scope);
}
/**
* Check whether scope has VOD stream with given name or not
*
* @param scope Scope
* @param name VOD stream name
*
* @return <code>true</code> if scope has VOD stream with given name,
* <code>false</code> otherwise.
*/
public boolean hasOnDemandStream(IScope scope, String name) {
IProviderService service = (IProviderService) getScopeService(scope,
IProviderService.class, ProviderService.class);
return (service.getVODProviderInput(scope, name) != null);
}
/**
* Returns VOD stream with given name from specified scope.
*
* @param scope Scope object
* @param name VOD stream name
*
* @return IOnDemandStream object that represents stream that can be played
* on demand, seekable and so forth. See {@link IOnDemandStream} for
* details.
*/
public IOnDemandStream getOnDemandStream(IScope scope, String name) {
log.warn("This won't work until the refactoring of the streaming code is complete.");
IOnDemandStreamService service = (IOnDemandStreamService) getScopeService(
scope, IOnDemandStreamService.class, StreamService.class, false);
return service.getOnDemandStream(scope, name);
}
/**
* Returns subscriber stream with given name from specified scope.
* Subscriber stream is a stream that clients can subscribe to.
*
* @param scope Scope
* @param name Stream name
*
* @return ISubscriberStream object
*/
public ISubscriberStream getSubscriberStream(IScope scope, String name) {
log.warn("This won't work until the refactoring of the streaming code is complete.");
ISubscriberStreamService service = (ISubscriberStreamService) getScopeService(
scope, ISubscriberStreamService.class, StreamService.class,
false);
return service.getSubscriberStream(scope, name);
}
/**
* Wrapper around ISchedulingService, adds a scheduled job to be run
* periodically. We store this service in the scope as it can be shared
* across all rooms of the applications.
*
* @param interval Time interval to run the scheduled job
* @param job Scheduled job object
*
* @return Name of the scheduled job
*/
public String addScheduledJob(int interval, IScheduledJob job) {
ISchedulingService service = (ISchedulingService) getScopeService(
scope, ISchedulingService.class, QuartzSchedulingService.class,
false);
return service.addScheduledJob(interval, job);
}
/**
* Adds a scheduled job that's gonna be executed once. Please note that the
* jobs are not saved if Red5 is restarted in the meantime.
*
* @param timeDelta Time offset in milliseconds from the current date when given
* job should be run
* @param job Scheduled job object
*
* @return Name of the scheduled job
*/
public String addScheduledOnceJob(long timeDelta, IScheduledJob job) {
ISchedulingService service = (ISchedulingService) getScopeService(
scope, ISchedulingService.class, QuartzSchedulingService.class,
false);
return service.addScheduledOnceJob(timeDelta, job);
}
/**
* Adds a scheduled job that's gonna be executed once on given date. Please
* note that the jobs are not saved if Red5 is restarted in the meantime.
*
* @param date When to run scheduled job
* @param job Scheduled job object
*
* @return Name of the scheduled job
*/
public String addScheduledOnceJob(Date date, IScheduledJob job) {
ISchedulingService service = (ISchedulingService) getScopeService(
scope, ISchedulingService.class, QuartzSchedulingService.class,
false);
return service.addScheduledOnceJob(date, job);
}
/**
* Adds a scheduled job which starts after the specified
* delay period and fires periodically.
*
* @param interval time in milliseconds between two notifications of the job
* @param job the job to trigger periodically
* @param delay time in milliseconds to pass before first execution.
* @return the name of the scheduled job
*/
public String addScheduledJobAfterDelay(int interval, IScheduledJob job, int delay) {
ISchedulingService service = (ISchedulingService) getScopeService(
scope, ISchedulingService.class, QuartzSchedulingService.class,
false);
return service.addScheduledJobAfterDelay(interval, job, delay);
}
/**
* Pauses a scheduled job
*
* @param name Scheduled job name
*/
public void pauseScheduledJob(String name) {
ISchedulingService service = (ISchedulingService) getScopeService(
scope, ISchedulingService.class, QuartzSchedulingService.class,
false);
service.pauseScheduledJob(name);
}
/**
* Resumes a scheduled job
*
* @param name Scheduled job name
*/
public void resumeScheduledJob(String name) {
ISchedulingService service = (ISchedulingService) getScopeService(
scope, ISchedulingService.class, QuartzSchedulingService.class,
false);
service.resumeScheduledJob(name);
}
/**
* Removes scheduled job from scheduling service list
*
* @param name Scheduled job name
*/
public void removeScheduledJob(String name) {
ISchedulingService service = (ISchedulingService) getScopeService(
scope, ISchedulingService.class, QuartzSchedulingService.class,
false);
service.removeScheduledJob(name);
}
/**
* Returns list of scheduled job names
*
* @return List of scheduled job names as list of Strings.
*/
public List<String> getScheduledJobNames() {
ISchedulingService service = (ISchedulingService) getScopeService(
scope, ISchedulingService.class, QuartzSchedulingService.class,
false);
return service.getScheduledJobNames();
}
// NOTE: Method added to get flv player to work.
/**
* Returns stream length. This is a hook so it may be removed.
*
* @param name Stream name
* @return Stream length in seconds (?)
*/
public double getStreamLength(String name) {
double duration = 0;
IProviderService provider = (IProviderService) getScopeService(scope,
IProviderService.class, ProviderService.class);
File file = provider.getVODProviderFile(scope, name);
if (file != null && file.canRead()) {
IStreamableFileFactory factory = (IStreamableFileFactory) ScopeUtils
.getScopeService(scope, IStreamableFileFactory.class,
StreamableFileFactory.class);
IStreamableFileService service = factory.getService(file);
if (service != null) {
ITagReader reader = null;
try {
IStreamableFile streamFile = service.getStreamableFile(file);
reader = streamFile.getReader();
duration = (double) reader.getDuration() / 1000;
} catch (IOException e) {
log.error("Error read stream file {}. {}", file.getAbsolutePath(), e);
} finally {
if (reader != null) {
reader.close();
}
}
} else {
log.error("No service found for {}", file.getAbsolutePath());
}
file = null;
}
return duration;
}
/** {@inheritDoc} */
public boolean clearSharedObjects(IScope scope, String name) {
ISharedObjectService service = (ISharedObjectService) getScopeService(
scope, ISharedObjectService.class, SharedObjectService.class,
false);
return service.clearSharedObjects(scope, name);
}
/**
* Client time to live is max allowed connection ping return time in seconds
* @return TTL value used in seconds
*/
public long getClientTTL() {
return clientTTL;
}
/**
* Client time to live is max allowed connection ping return time in seconds
* @param clientTTL New TTL value in seconds
*/
public void setClientTTL(int clientTTL) {
this.clientTTL = clientTTL;
}
/**
* Return period of ghost connections cleanup task call
* @return Ghost connections cleanup period
*/
public int getGhostConnsCleanupPeriod() {
return ghostConnsCleanupPeriod;
}
/**
* Set new ghost connections cleanup period
* @param ghostConnsCleanupPeriod New ghost connections cleanup period
*/
public void setGhostConnsCleanupPeriod(int ghostConnsCleanupPeriod) {
this.ghostConnsCleanupPeriod = ghostConnsCleanupPeriod;
}
/**
* Schedules new ghost connections cleanup using current cleanup period
*/
public void scheduleGhostConnectionsCleanup() {
IScheduledJob job = new IScheduledJob(){
public void execute(ISchedulingService service) throws CloneNotSupportedException {
killGhostConnections();
}
};
// Cancel previous if was scheduled
cancelGhostConnectionsCleanup();
// Store name so we can cancel it later
ghostCleanupJobName = schedulingService.addScheduledJob(ghostConnsCleanupPeriod * 1000, job);
}
/**
* Cancel ghost connections cleanup period
*/
public void cancelGhostConnectionsCleanup() {
if (ghostCleanupJobName != null) {
schedulingService.removeScheduledJob(ghostCleanupJobName);
}
}
/**
* Cleans up ghost connections
*/
protected void killGhostConnections() {
Collection<Set<IConnection>> conns = getConnections();
for (Set<IConnection> set : conns) {
for (IConnection conn : set) {
// Ping client
conn.ping();
// FIXME: getLastPingTime doesn't get updated right after conn.ping()
// Time to live exceeded, disconnect
if (conn.getLastPingTime() > clientTTL * 1000) {
disconnect(conn, scope);
}
}
}
}
/**
* Notification method that is sent by FME just before publishing starts.
*
* @param streamName Name of stream that is about to be published.
*/
public void FCPublish(String streamName) {
// Override if necessary.
}
/**
* Notification method that is sent by FME when publishing of a stream ends.
*/
public void FCUnpublish() {
// Override if necessary.
}
public void streamBroadcastClose(IBroadcastStream stream) {
//log w3c connect event
IConnection conn = Red5.getConnectionLocal();
long publishDuration = -1;
if (stream instanceof ClientBroadcastStream) {
//converted to seconds
publishDuration = (System.currentTimeMillis() - ((ClientBroadcastStream) stream).getCreationTime()) / 1000;
}
log.info("W3C x-category:stream x-event:unpublish c-ip:{} cs-bytes:{} sc-bytes:{} x-sname:{} x-file-length:{} x-name:{}", new Object[]{conn.getRemoteAddress(), conn.getReadBytes(), conn.getWrittenBytes(), stream.getName(), publishDuration, stream.getPublishedName()});
String recordingName = stream.getSaveFilename();
//if its not null then we did a recording
if (recordingName != null) {
//use cs-bytes for file size for now
log.info("W3C x-category:stream x-event:recordstop c-ip:{} cs-bytes:{} sc-bytes:{} x-sname:{} x-file-name:{} x-file-length:{} x-file-size:{}", new Object[]{conn.getRemoteAddress(), conn.getReadBytes(), conn.getWrittenBytes(), stream.getName(), recordingName, publishDuration, conn.getReadBytes()});
//if the stream length is 0 bytes then delete it, this
//is a fix for SN-20
//get the web root
String webappsPath = System.getProperty("red5.webapp.root");
//add context name
File file = new File(webappsPath, getName() + '/' + recordingName);
if (file != null) {
log.debug("File path: {}", file.getAbsolutePath());
if (file.exists()) {
//if publish duration or length are zero
if (publishDuration == 0 || file.length() == 0) {
if (file.delete()) {
log.info("File {} was deleted", file.getName());
} else {
log.info("File {} was not deleted, it will be deleted on exit", file.getName());
file.deleteOnExit();
}
}
}
file = null;
}
}
}
public void streamBroadcastStart(IBroadcastStream stream) {
}
public void streamPlaylistItemPlay(IPlaylistSubscriberStream stream, IPlayItem item, boolean isLive) {
//log w3c connect event
log.info("W3C x-category:stream x-event:play c-ip:{} x-sname:{} x-name:{}", new Object[]{Red5.getConnectionLocal().getRemoteAddress(), stream.getName(), item.getName()});
}
public void streamPlaylistItemStop(IPlaylistSubscriberStream stream, IPlayItem item) {
//since there is a fair amount of processing below we will check log level prior to proceeding
if (log.isInfoEnabled()) {
//log w3c connect event
String remoteAddress = "";
long readBytes = -1;
long writtenBytes = -1;
IConnection conn = Red5.getConnectionLocal();
if (conn != null) {
remoteAddress = conn.getRemoteAddress();
readBytes = conn.getReadBytes();
writtenBytes = conn.getWrittenBytes();
}
long playDuration = -1;
if (stream instanceof PlaylistSubscriberStream) {
//converted to seconds
playDuration = (System.currentTimeMillis() - ((PlaylistSubscriberStream) stream).getCreationTime()) / 1000;
}
long playItemSize = -1;
String playItemName = "";
if (item != null) {
playItemSize = item.getSize();
playItemName = item.getName();
}
log.info("W3C x-category:stream x-event:stop c-ip:{} cs-bytes:{} sc-bytes:{} x-sname:{} x-file-length:{} x-file-size:{} x-name:{}", new Object[]{remoteAddress, readBytes, writtenBytes, stream.getName(), playDuration, playItemSize, playItemName});
}
}
public void streamPlaylistVODItemPause(IPlaylistSubscriberStream stream, IPlayItem item, int position) {
//log w3c connect event
log.info("W3C x-category:stream x-event:pause c-ip:{} x-sname:{}", Red5.getConnectionLocal().getRemoteAddress(), stream.getName());
}
public void streamPlaylistVODItemResume(IPlaylistSubscriberStream stream, IPlayItem item, int position) {
//log w3c connect event
log.info("W3C x-category:stream x-event:unpause c-ip:{} x-sname:{}", Red5.getConnectionLocal().getRemoteAddress(), stream.getName());
}
public void streamPlaylistVODItemSeek(IPlaylistSubscriberStream stream, IPlayItem item, int position) {
// Override if necessary.
}
public void streamPublishStart(IBroadcastStream stream) {
//log w3c connect event
IConnection connection = Red5.getConnectionLocal();
log.info("W3C x-category:stream x-event:publish c-ip:{} x-sname:{} x-name:{}", new Object[]{connection != null ? connection.getRemoteAddress() : "0.0.0.0", stream.getName(), stream.getPublishedName()});
}
public void streamRecordStart(IBroadcastStream stream) {
//log w3c connect event
IConnection connection = Red5.getConnectionLocal();
log.info("W3C x-category:stream x-event:record c-ip:{} x-sname:{} x-file-name:{}", new Object[]{connection != null ? connection.getRemoteAddress() : "0.0.0.0", stream.getName(), stream.getSaveFilename()});
}
public void streamSubscriberClose(ISubscriberStream stream) {
//log w3c connect event
IConnection conn = Red5.getConnectionLocal();
log.info("W3C x-category:stream x-event:stop c-ip:{} cs-bytes:{} sc-bytes:{} x-sname:{}", new Object[]{conn.getRemoteAddress(), conn.getReadBytes(), conn.getWrittenBytes(), stream.getName()});
}
public void streamSubscriberStart(ISubscriberStream stream) {
//log w3c connect event
log.info("W3C x-category:stream x-event:play c-ip:{} x-sname:{}", Red5.getConnectionLocal().getRemoteAddress(), stream.getName());
}
}