/*
* RED5 Open Source Flash Server - http://code.google.com/p/red5/
*
* Copyright 2006-2014 by respective authors (see below). All rights reserved.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.red5.server.adapter;
import java.io.File;
import java.io.IOException;
import java.lang.reflect.Method;
import java.util.Collection;
import java.util.Collections;
import java.util.Date;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.concurrent.CopyOnWriteArraySet;
import org.red5.io.IStreamableFile;
import org.red5.io.ITagReader;
import org.red5.logging.Red5LoggerFactory;
import org.red5.server.api.IClient;
import org.red5.server.api.IConnection;
import org.red5.server.api.Red5;
import org.red5.server.api.event.IEvent;
import org.red5.server.api.plugin.IRed5Plugin;
import org.red5.server.api.plugin.IRed5PluginHandler;
import org.red5.server.api.scheduling.IScheduledJob;
import org.red5.server.api.scheduling.ISchedulingService;
import org.red5.server.api.scope.IBroadcastScope;
import org.red5.server.api.scope.IScope;
import org.red5.server.api.service.IBroadcastStreamService;
import org.red5.server.api.service.IOnDemandStreamService;
import org.red5.server.api.service.IStreamSecurityService;
import org.red5.server.api.service.IStreamableFileService;
import org.red5.server.api.service.ISubscriberStreamService;
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.IOnDemandStream;
import org.red5.server.api.stream.IPlayItem;
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.IStreamService;
import org.red5.server.api.stream.IStreamableFileFactory;
import org.red5.server.api.stream.ISubscriberStream;
import org.red5.server.exception.ClientRejectedException;
import org.red5.server.jmx.mxbeans.ApplicationMXBean;
import org.red5.server.messaging.AbstractPipe;
import org.red5.server.messaging.IMessageInput;
import org.red5.server.plugin.PluginDescriptor;
import org.red5.server.plugin.PluginRegistry;
import org.red5.server.plugin.Red5Plugin;
import org.red5.server.scheduling.QuartzSchedulingService;
import org.red5.server.so.SharedObjectService;
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.red5.server.stream.StreamableFileFactory;
import org.red5.server.util.ScopeUtils;
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 />
* }
* </code>
*
* <p>
* This method added to ApplicationAdapter subclass 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
* @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, ApplicationMXBean {
/**
* Logger object
*/
protected Logger log = null;
/**
* List of application listeners.
*/
private CopyOnWriteArraySet<IApplication> listeners = new CopyOnWriteArraySet<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 a listener that will get notified about application events.
*
* @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) {
// ensure the log is not null at this point
if (log == null) {
log = Red5LoggerFactory.getLogger(this.getClass());
}
log.debug("connect: {} > {}", conn, scope);
boolean success = false;
// hit the super class first
if (super.connect(conn, scope, params)) {
if (log.isInfoEnabled() && ScopeUtils.isApp(scope)) {
// 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());
}
}
if (ScopeUtils.isApp(scope)) {
success = appConnect(conn, params);
} else if (ScopeUtils.isRoom(scope)) {
success = roomConnect(conn, params);
} else {
log.warn("Scope was not of app or room type, connect failed");
}
}
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) {
//set the log here so that stuff is logged in the correct place
if (log == null) {
log = Red5LoggerFactory.getLogger(this.getClass());
}
log.debug("start: {}", scope);
//setup plug-ins
log.trace("Plugins: {}", plugins);
if (plugins != null) {
for (PluginDescriptor desc : plugins) {
log.debug("Plugin: {}", desc);
try {
//ensure plug-in class can be resolved
ClassLoader classLoader = scope.getClassLoader();
Class<?> clazz = Class.forName(desc.getPluginType(), true, classLoader);
log.trace("Class: {}", clazz);
//get the plug-in from the registry
IRed5Plugin plugin = PluginRegistry.getPlugin(desc.getPluginName());
log.debug("Got plugin from the registry: {}", plugin);
//if the plugin class extends the red5 plug-in we will add the application to it
if (plugin instanceof Red5Plugin) {
//pass the app to the plugin so that it may be manipulated directly by the plug-in
((Red5Plugin) plugin).setApplication(this);
}
//when a plug-in method is specified do invokes
if (desc.getMethod() != null) {
Method method = plugin.getClass().getMethod(desc.getMethod(), (Class[]) null);
//if a return type is not specified for the plug-in method just invoke it
if (desc.getMethodReturnType() == null) {
log.debug("Invoking plugin");
method.invoke(plugin, (Object[]) null);
} else {
log.debug("Invoking plugin");
Object returnClass = method.invoke(plugin, (Object[]) null);
if (returnClass instanceof IRed5PluginHandler) {
//if there are props add them
Map<String, Object> props = desc.getProperties();
if (props != null) {
Method setProps = returnClass.getClass().getMethod("setProperties", new Class[] { Map.class });
setProps.invoke(returnClass, new Object[] { props });
}
//initialize
Method init = returnClass.getClass().getMethod("init", (Class[]) null);
init.invoke(returnClass, (Object[]) null);
}
if (returnClass instanceof IApplication) {
//if its an IApplcation add it to the listeners
log.debug("Adding result class to listeners");
addListener((IApplication) returnClass);
} else {
log.info("Returned class did not implement IApplication");
}
}
}
} catch (Exception e) {
log.warn("Exception setting up a plugin", e);
}
}
}
boolean started = false;
// verify that we can start
if (super.start(scope)) {
if (ScopeUtils.isApp(scope)) {
started = appStart(scope);
} else if (ScopeUtils.isRoom(scope)) {
started = roomStart(scope);
} else {
log.warn("Scope wasn't of app or room type, it was not started");
}
// fix for issue 91
if (started) {
// we dont allow connections until we are started
super.setCanConnect(true);
// we also dont allow service calls until started
super.setCanCallService(true);
}
}
return started;
}
/**
* 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() && ScopeUtils.isApp(scope)) {
// 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 (ScopeUtils.isApp(scope)) {
appDisconnect(conn);
} else if (ScopeUtils.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) {
log.debug("stop: {}", scope.getName());
// stop the app / room / etc
if (ScopeUtils.isApp(scope)) {
// we don't allow connections after we stop
super.setCanConnect(false);
// we also don't allow service calls
super.setCanCallService(false);
// stop the app
appStop(scope);
} else if (ScopeUtils.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 (ScopeUtils.isApp(scope)) {
return appJoin(client, scope);
} else {
return ScopeUtils.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 (ScopeUtils.isApp(scope)) {
appLeave(client, scope);
} else if (ScopeUtils.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) {
if (!listener.appStart(app)) {
return false;
}
}
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) {
if (!listener.roomStart(room)) {
return false;
}
}
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 the 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) log.debug("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) {
if (!listener.appConnect(conn, params)) {
return false;
}
}
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) {
if (!listener.roomConnect(conn, params)) {
return false;
}
}
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) {
if (!listener.appJoin(client, app)) {
return false;
}
}
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) {
if (!listener.roomJoin(client, room)) {
return false;
}
}
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 stateful, 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 prefer 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) ScopeUtils.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) ScopeUtils.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) ScopeUtils.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) ScopeUtils.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) ScopeUtils.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) ScopeUtils.getScopeService(scope, IProviderService.class, ProviderService.class);
return (service.getLiveProviderInput(scope, name, false) != null);
}
/** {@inheritDoc} */
public IBroadcastStream getBroadcastStream(IScope scope, String name) {
IStreamService service = (IStreamService) ScopeUtils.getScopeService(scope, IStreamService.class, StreamService.class);
if (service instanceof StreamService) {
IBroadcastScope bs = ((StreamService) service).getBroadcastScope(scope, name);
if (bs != null) {
return bs.getClientBroadcastStream();
}
}
return null;
}
/**
* 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 Set<String> getBroadcastStreamNames(IScope scope) {
IProviderService service = (IProviderService) ScopeUtils.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) ScopeUtils.getScopeService(scope, IProviderService.class, ProviderService.class);
IMessageInput msgIn = service.getVODProviderInput(scope, name);
if (msgIn instanceof AbstractPipe) {
((AbstractPipe) msgIn).close();
}
return (msgIn != 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) ScopeUtils.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) ScopeUtils.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) ScopeUtils.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) ScopeUtils.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) ScopeUtils.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) ScopeUtils.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) ScopeUtils.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) ScopeUtils.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) ScopeUtils.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) ScopeUtils.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) ScopeUtils.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) ScopeUtils.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) {
log.info("TTL exceeded, disconnecting {}", conn);
disconnect(conn, scope);
}
}
}
}
/**
* Start transmission notification from Flash Player 11.1+. This command asks the server to transmit more data because the buffer is running low.
*
* http://help.adobe.com/en_US/flashmediaserver/devguide/WSd391de4d9c7bd609-569139412a3743e78e-8000.html
*
* @param bool
* @param num
*/
public void startTransmit(Boolean bool, int num) {
}
/**
* Stop transmission notification from Flash Player 11.1+. This command asks the server to suspend transmission until the client sends a
* startTransmit event because there is enough data in the buffer.
*/
public void stopTransmit() {
}
/**
* Stop transmission notification from Flash Player 11.1+. This command asks the server to suspend transmission until the client sends a
* startTransmit event because there is enough data in the buffer.
*
* @param bool
* @param num
*/
public void stopTransmit(Boolean bool, int num) {
}
/**
* 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) {
}
/**
* Notification method that is sent by FME when publishing of a stream ends.
*/
public void FCUnpublish() {
}
/**
* Notification method that is sent by FME when publishing of a stream ends.
*/
public void FCUnpublish(String streamName) {
}
/**
* Notification method that is sent by some clients just before playback starts.
*
* @param streamName
* Name of stream that is about to be played.
*/
public void FCSubscribe(String streamName) {
}
/**
* Notification that a broadcasting stream is closing.
*
* @param stream
*/
public void streamBroadcastClose(IBroadcastStream stream) {
// log w3c connect event
IConnection conn = Red5.getConnectionLocal();
// converted to seconds
long publishDuration = (System.currentTimeMillis() - stream.getCreationTime()) / 1000;
if (conn != null) {
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() });
} else {
log.info("W3C x-category:stream x-event:unpublish x-sname:{} x-file-length:{} x-name:{}", new Object[] { stream.getName(), publishDuration, stream.getPublishedName() });
}
String recordingName = stream.getSaveFilename();
// if its not null then we did a recording
if (recordingName != null) {
if (conn != 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() });
} else {
log.info("W3C x-category:stream x-event:recordstop x-sname:{} x-file-name:{} x-file-length:{}", new Object[] { stream.getName(), recordingName, publishDuration });
}
// 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 streamPlayItemPlay(ISubscriberStream 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 streamPlayItemStop(ISubscriberStream 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) {
playItemName = item.getName();
//get file size in bytes if available
IProviderService providerService = (IProviderService) scope.getContext().getBean(IProviderService.BEAN_NAME);
if (providerService != null) {
File file = providerService.getVODProviderFile(scope, playItemName);
if (file != null) {
playItemSize = file.length();
} else {
log.debug("File was null, this is ok for live streams");
}
} else {
log.debug("ProviderService was null");
}
}
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 streamPlayItemPause(ISubscriberStream 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 streamPlayItemResume(ISubscriberStream 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 streamPlayItemSeek(ISubscriberStream 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-start c-ip:{} x-sname:{} x-file-name:{}",
new Object[] { connection != null ? connection.getRemoteAddress() : "0.0.0.0", stream.getName(), stream.getSaveFilename() });
}
public void streamRecordStop(IBroadcastStream stream) {
// log w3c connect event
IConnection connection = Red5.getConnectionLocal();
log.info("W3C x-category:stream x-event:record-stop 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());
}
@Override
public boolean handleEvent(IEvent event) {
log.debug("handleEvent: {}", event);
return super.handleEvent(event);
}
}