package org.red5.server.stream; /* * 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 java.io.File; import java.io.IOException; import java.util.Set; import org.apache.commons.lang.StringUtils; import org.red5.server.BaseConnection; import org.red5.server.api.IBasicScope; import org.red5.server.api.IConnection; import org.red5.server.api.IContext; import org.red5.server.api.IScope; import org.red5.server.api.Red5; import org.red5.server.api.ScopeUtils; import org.red5.server.api.stream.IBroadcastStream; import org.red5.server.api.stream.IClientBroadcastStream; import org.red5.server.api.stream.IClientStream; import org.red5.server.api.stream.IPlaylistSubscriberStream; import org.red5.server.api.stream.ISingleItemSubscriberStream; import org.red5.server.api.stream.IStreamCapableConnection; 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.OperationNotSupportedException; import org.red5.server.api.stream.support.SimplePlayItem; import org.red5.server.net.rtmp.BaseRTMPHandler; import org.red5.server.net.rtmp.Channel; import org.red5.server.net.rtmp.RTMPConnection; import org.red5.server.net.rtmp.status.Status; import org.red5.server.net.rtmp.status.StatusCodes; import org.slf4j.Logger; import org.slf4j.LoggerFactory; /** * Stream service */ public class StreamService implements IStreamService { private static Logger logger = LoggerFactory.getLogger(StreamService.class); /** {@inheritDoc} */ public void closeStream() { IConnection conn = Red5.getConnectionLocal(); if (!(conn instanceof IStreamCapableConnection)) { return; } IClientStream stream = ((IStreamCapableConnection) conn) .getStreamById(getCurrentStreamId()); if (stream != null) { if (stream instanceof IClientBroadcastStream) { IClientBroadcastStream bs = (IClientBroadcastStream) stream; IBroadcastScope bsScope = getBroadcastScope(conn.getScope(), bs .getPublishedName()); if (bsScope != null && conn instanceof BaseConnection) { ((BaseConnection) conn).unregisterBasicScope(bsScope); } } stream.close(); } ((IStreamCapableConnection) conn) .deleteStreamById(getCurrentStreamId()); } /** * Close stream. * This method can close both IClientBroadcastStream (coming from Flash Player to Red5) * and ISubscriberStream (from Red5 to Flash Player). * Corresponding application handlers (streamSubscriberClose, etc.) are called as if * close was initiated by Flash Player. * * It is recommended to remember stream id in application handlers, ex.: * <pre> * public void streamBroadcastStart(IBroadcastStream stream) { * super.streamBroadcastStart(stream); * if (stream instanceof IClientBroadcastStream) { * int publishedStreamId = ((ClientBroadcastStream)stream).getStreamId(); * Red5.getConnectionLocal().setAttribute(PUBLISHED_STREAM_ID_ATTRIBUTE, publishedStreamId); * } * } * </pre> * <pre> * public void streamPlaylistItemPlay(IPlaylistSubscriberStream stream, IPlayItem item, boolean isLive) { * super.streamPlaylistItemPlay(stream, item, isLive); * Red5.getConnectionLocal().setAttribute(WATCHED_STREAM_ID_ATTRIBUTE, stream.getStreamId()); * } * </pre> * When stream is closed, corresponding NetStream status will be sent to stream provider / consumers. * Implementation is based on Red5's StreamService.close() * @param connection client connection * @param streamId stream ID (number: 1,2,...) */ public static void closeStream(IConnection connection, int streamId) { if (!(connection instanceof IStreamCapableConnection)) { logger.warn("Connection is not instance of IStreamCapableConnection: {}", connection); return; } IStreamCapableConnection scConnection = (IStreamCapableConnection) connection; IClientStream stream = scConnection.getStreamById(streamId); if (stream == null) { logger.info("Stream not found: streamId={}, connection={}", streamId, connection); return; } if (stream instanceof IClientBroadcastStream) { // this is a broadcasting stream (from Flash Player to Red5) IClientBroadcastStream bs = (IClientBroadcastStream) stream; IBroadcastScope bsScope = (IBroadcastScope)connection.getScope().getBasicScope( IBroadcastScope.TYPE, bs.getPublishedName()); if (bsScope != null && connection instanceof BaseConnection) { ((BaseConnection) connection).unregisterBasicScope(bsScope); } } stream.close(); scConnection.deleteStreamById(streamId); // in case of broadcasting stream, status is sent automatically by Red5 if (!(stream instanceof IClientBroadcastStream)) { StreamService.sendNetStreamStatus(connection, StatusCodes.NS_PLAY_STOP, "Stream closed by server", stream.getName(), Status.STATUS, streamId); } } /** {@inheritDoc} */ public int createStream() { IConnection conn = Red5.getConnectionLocal(); if (!(conn instanceof IStreamCapableConnection)) { return -1; } return ((IStreamCapableConnection) conn).reserveStreamId(); } /** {@inheritDoc} */ public void deleteStream(int streamId) { IConnection conn = Red5.getConnectionLocal(); if (!(conn instanceof IStreamCapableConnection)) { return; } IStreamCapableConnection streamConn = (IStreamCapableConnection) conn; deleteStream(streamConn, streamId); } /** {@inheritDoc} */ public void deleteStream(IStreamCapableConnection conn, int streamId) { IClientStream stream = conn.getStreamById(streamId); if (stream != null) { if (stream instanceof IClientBroadcastStream) { IClientBroadcastStream bs = (IClientBroadcastStream) stream; IBroadcastScope bsScope = getBroadcastScope(conn.getScope(), bs .getPublishedName()); if (bsScope != null && conn instanceof BaseConnection) { ((BaseConnection) conn).unregisterBasicScope(bsScope); } } stream.close(); } conn.unreserveStreamId(streamId); } /** {@inheritDoc} */ public void releaseStream(String streamName) { // XXX: what to do here? } /** {@inheritDoc} */ public void pause(boolean pausePlayback, int position) { pause(Boolean.valueOf(pausePlayback), position); } /** {@inheritDoc} */ public void pauseRaw(boolean pausePlayback, int position) { pause(pausePlayback, position); } /** * Pause at given position. Required as "pausePlayback" can be "null" if no flag is passed by the * client * @param pausePlayback Pause playback or not * @param position Pause position */ public void pause(Boolean pausePlayback, int position) { IConnection conn = Red5.getConnectionLocal(); if (!(conn instanceof IStreamCapableConnection)) { return; } IStreamCapableConnection streamConn = (IStreamCapableConnection) conn; int streamId = getCurrentStreamId(); IClientStream stream = streamConn.getStreamById(streamId); if (stream == null || !(stream instanceof ISubscriberStream)) { return; } ISubscriberStream subscriberStream = (ISubscriberStream) stream; // pausePlayback can be "null" if "pause" is called without any parameters from flash if (pausePlayback == null) { pausePlayback = !subscriberStream.isPaused(); } if (pausePlayback) { subscriberStream.pause(position); } else { subscriberStream.resume(position); } } public void play(String name, int start, int length, Object flushPlaylist) { if (flushPlaylist instanceof Boolean) { play(name, start, length, ((Boolean) flushPlaylist).booleanValue()); } else { play(name, start, length); } } /** {@inheritDoc} */ public void play(String name, int start, int length, boolean flushPlaylist) { logger.debug("Play called - name: {} start: {} length: {} flush playlist: {}", new Object[]{name, start, length, flushPlaylist}); IConnection conn = Red5.getConnectionLocal(); if (!(conn instanceof IStreamCapableConnection)) { return; } IScope scope = conn.getScope(); IStreamCapableConnection streamConn = (IStreamCapableConnection) conn; int streamId = getCurrentStreamId(); if (StringUtils.isEmpty(name)) { sendNSFailed((RTMPConnection) streamConn, "The stream name may not be empty.", name, streamId); return; } IStreamSecurityService security = (IStreamSecurityService) ScopeUtils.getScopeService(scope, IStreamSecurityService.class); if (security != null) { Set<IStreamPlaybackSecurity> handlers = security.getStreamPlaybackSecurity(); for (IStreamPlaybackSecurity handler: handlers) { if (!handler.isPlaybackAllowed(scope, name, start, length, flushPlaylist)) { sendNSFailed((RTMPConnection) streamConn, "You are not allowed to play the stream.", name, streamId); return; } } } IClientStream stream = streamConn.getStreamById(streamId); boolean created = false; if (stream == null) { stream = streamConn.newPlaylistSubscriberStream(streamId); stream.start(); created = true; } if (!(stream instanceof ISubscriberStream)) { return; } ISubscriberStream subscriberStream = (ISubscriberStream) stream; SimplePlayItem item = new SimplePlayItem(); item.setName(name); item.setStart(start); item.setLength(length); //get file size in bytes if available IProviderService providerService = (IProviderService) scope.getContext().getBean(IProviderService.BEAN_NAME); if (providerService != null) { File file = providerService.getVODProviderFile(scope, name); if (file != null) { item.setSize(file.length()); } else { logger.debug("File was null, this is ok for live streams"); } } else { logger.debug("ProviderService was null"); } if (subscriberStream instanceof IPlaylistSubscriberStream) { IPlaylistSubscriberStream playlistStream = (IPlaylistSubscriberStream) subscriberStream; if (flushPlaylist) { playlistStream.removeAllItems(); } playlistStream.addItem(item); } else if (subscriberStream instanceof ISingleItemSubscriberStream) { ISingleItemSubscriberStream singleStream = (ISingleItemSubscriberStream) subscriberStream; singleStream.setPlayItem(item); } else { // not supported by this stream service return; } try { subscriberStream.play(); } catch (IOException err) { if (created) { stream.close(); streamConn.deleteStreamById(streamId); } sendNSFailed((RTMPConnection) streamConn, err.getMessage(), name, streamId); } } /** {@inheritDoc} */ public void play(String name, int start, int length) { play(name, start, length, true); } /** {@inheritDoc} */ public void play(String name, int start) { play(name, start, -1000, true); } /** {@inheritDoc} */ public void play(String name) { play(name, -2000, -1000, true); } /** {@inheritDoc} */ public void play(Boolean dontStop) { logger.debug("Play called. Dont stop param: {}", dontStop); if (!dontStop) { IConnection conn = Red5.getConnectionLocal(); if (!(conn instanceof IStreamCapableConnection)) { return; } IStreamCapableConnection streamConn = (IStreamCapableConnection) conn; int streamId = getCurrentStreamId(); IClientStream stream = streamConn.getStreamById(streamId); if (stream != null) { stream.stop(); } } } /** {@inheritDoc} */ public void publish(Boolean dontStop) { if (!dontStop) { IConnection conn = Red5.getConnectionLocal(); if (!(conn instanceof IStreamCapableConnection)) { return; } IStreamCapableConnection streamConn = (IStreamCapableConnection) conn; int streamId = getCurrentStreamId(); IClientStream stream = streamConn.getStreamById(streamId); if (!(stream instanceof IBroadcastStream)) { return; } IBroadcastStream bs = (IBroadcastStream) stream; if (bs.getPublishedName() == null) { return; } IBroadcastScope bsScope = getBroadcastScope(conn.getScope(), bs .getPublishedName()); if (bsScope != null) { bsScope.unsubscribe(bs.getProvider()); if (conn instanceof BaseConnection) { ((BaseConnection) conn).unregisterBasicScope(bsScope); } } bs.close(); streamConn.deleteStreamById(streamId); } } /** {@inheritDoc} */ public void publish(String name, String mode) { IConnection conn = Red5.getConnectionLocal(); if (!(conn instanceof IStreamCapableConnection)) { return; } IScope scope = conn.getScope(); IStreamCapableConnection streamConn = (IStreamCapableConnection) conn; int streamId = getCurrentStreamId(); if (StringUtils.isEmpty(name)) { sendNSFailed((RTMPConnection) streamConn, "The stream name may not be empty.", name, streamId); return; } IStreamSecurityService security = (IStreamSecurityService) ScopeUtils.getScopeService(scope, IStreamSecurityService.class); if (security != null) { Set<IStreamPublishSecurity> handlers = security.getStreamPublishSecurity(); for (IStreamPublishSecurity handler: handlers) { if (!handler.isPublishAllowed(scope, name, mode)) { sendNSFailed((RTMPConnection) streamConn, "You are not allowed to publish the stream.", name, streamId); return; } } } IBroadcastScope bsScope = getBroadcastScope(scope, name); if (bsScope != null && !bsScope.getProviders().isEmpty()) { // Another stream with that name is already published. Status badName = new Status(StatusCodes.NS_PUBLISH_BADNAME); badName.setClientid(streamId); badName.setDetails(name); badName.setLevel("error"); // FIXME: there should be a direct way to send the status Channel channel = ((RTMPConnection) streamConn).getChannel((byte) (4 + ((streamId-1) * 5))); channel.sendStatus(badName); return; } IClientStream stream = streamConn.getStreamById(streamId); if (stream != null && !(stream instanceof IClientBroadcastStream)) { return; } boolean created = false; if (stream == null) { stream = streamConn.newBroadcastStream(streamId); created = true; } IClientBroadcastStream bs = (IClientBroadcastStream) stream; try { bs.setPublishedName(name); IContext context = conn.getScope().getContext(); IProviderService providerService = (IProviderService) context .getBean(IProviderService.BEAN_NAME); // TODO handle registration failure if (providerService.registerBroadcastStream(conn.getScope(), name, bs)) { bsScope = getBroadcastScope(conn.getScope(), name); bsScope.setAttribute(IBroadcastScope.STREAM_ATTRIBUTE, bs); if (conn instanceof BaseConnection) { ((BaseConnection) conn).registerBasicScope(bsScope); } } logger.debug("Mode: {}", mode); if (IClientStream.MODE_RECORD.equals(mode)) { bs.start(); bs.saveAs(name, false); } else if (IClientStream.MODE_APPEND.equals(mode)) { bs.start(); bs.saveAs(name, true); } else if (IClientStream.MODE_PUBLISH.equals(mode) || IClientStream.MODE_LIVE.equals(mode)) { bs.start(); } bs.startPublishing(); } catch (IOException e) { Status accessDenied = new Status(StatusCodes.NS_RECORD_NOACCESS); accessDenied.setClientid(streamId); accessDenied.setDesciption("The file could not be created/written to."); accessDenied.setDetails(name); accessDenied.setLevel("error"); // FIXME: there should be a direct way to send the status Channel channel = ((RTMPConnection) streamConn).getChannel((byte) (4 + ((streamId-1) * 5))); channel.sendStatus(accessDenied); bs.close(); if (created) { streamConn.deleteStreamById(streamId); } return; } catch (Exception e) { logger.warn("Exception on publish", e); } } /** {@inheritDoc} */ public void publish(String name) { publish(name, IClientStream.MODE_LIVE); } /** {@inheritDoc} */ public void seek(int position) { IConnection conn = Red5.getConnectionLocal(); if (!(conn instanceof IStreamCapableConnection)) { return; } IStreamCapableConnection streamConn = (IStreamCapableConnection) conn; int streamId = getCurrentStreamId(); IClientStream stream = streamConn.getStreamById(streamId); if (stream == null || !(stream instanceof ISubscriberStream)) { return; } ISubscriberStream subscriberStream = (ISubscriberStream) stream; try { subscriberStream.seek(position); } catch (OperationNotSupportedException err) { Status seekFailed = new Status(StatusCodes.NS_SEEK_FAILED); seekFailed.setClientid(streamId); seekFailed.setDesciption("The stream doesn't support seeking."); seekFailed.setLevel("error"); // FIXME: there should be a direct way to send the status Channel channel = ((RTMPConnection) streamConn).getChannel((byte) (4 + ((streamId-1) * 5))); channel.sendStatus(seekFailed); } } /** {@inheritDoc} */ public void receiveVideo(boolean receive) { IConnection conn = Red5.getConnectionLocal(); if (!(conn instanceof IStreamCapableConnection)) { return; } IStreamCapableConnection streamConn = (IStreamCapableConnection) conn; int streamId = getCurrentStreamId(); IClientStream stream = streamConn.getStreamById(streamId); if (stream == null || !(stream instanceof ISubscriberStream)) { return; } ISubscriberStream subscriberStream = (ISubscriberStream) stream; subscriberStream.receiveVideo(receive); } /** {@inheritDoc} */ public void receiveAudio(boolean receive) { IConnection conn = Red5.getConnectionLocal(); if (!(conn instanceof IStreamCapableConnection)) { return; } IStreamCapableConnection streamConn = (IStreamCapableConnection) conn; int streamId = getCurrentStreamId(); IClientStream stream = streamConn.getStreamById(streamId); if (stream == null || !(stream instanceof ISubscriberStream)) { return; } ISubscriberStream subscriberStream = (ISubscriberStream) stream; subscriberStream.receiveAudio(receive); } /** * Getter for current stream id. * * @return Current stream id */ private int getCurrentStreamId() { // TODO: this must come from the current connection! return BaseRTMPHandler.getStreamId(); } /** * Return broadcast scope object for given scope and child scope name * @param scope Scope object * @param name Child scope name * @return Broadcast scope */ public IBroadcastScope getBroadcastScope(IScope scope, String name) { IBasicScope basicScope = scope.getBasicScope(IBroadcastScope.TYPE, name); if (!(basicScope instanceof IBroadcastScope)) { return null; } else { return (IBroadcastScope) basicScope; } } /** * Send a <code>NetStream.Failed</code> message to the client. * * @param conn * @param description * @param name * @param streamId */ private void sendNSFailed(RTMPConnection conn, String description, String name, int streamId) { StreamService.sendNetStreamStatus(conn, StatusCodes.NS_FAILED, description, name, Status.ERROR, streamId); } /** * Send <code>NetStream.Status</code> to client (Flash Player) * @param conn * @param statusCode see StatusCodes class * @param description * @param name * @param streamId */ @SuppressWarnings("unused") private void sendNSStatus(IConnection conn, String statusCode, String description, String name, int streamId) { StreamService.sendNetStreamStatus(conn, statusCode, description, name, Status.STATUS, streamId); } /** * Send <code>NetStream.Status</code> to client (Flash Player) * * @param conn connection * @param statusCode NetStream status code * @param description description * @param name name * @param status The status - error, warning, or status * @param streamId stream id */ public static void sendNetStreamStatus(IConnection conn, String statusCode, String description, String name, String status, int streamId) { if (!(conn instanceof RTMPConnection)) { throw new RuntimeException("Connection is not RTMPConnection: " + conn); } Status s = new Status(statusCode); s.setClientid(streamId); s.setDesciption(description); s.setDetails(name); s.setLevel(status); Channel channel = ((RTMPConnection) conn).getChannel((byte) (4 + ((streamId - 1) * 5))); channel.sendStatus(s); } }