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.IOException;
import java.util.HashMap;
import java.util.Map;
import java.util.concurrent.ScheduledFuture;
import java.util.concurrent.TimeUnit;
import org.apache.mina.common.ByteBuffer;
import org.red5.io.amf.Output;
import org.red5.io.object.Serializer;
import org.red5.logging.Red5LoggerFactory;
import org.red5.server.api.IScope;
import org.red5.server.api.scheduling.IScheduledJob;
import org.red5.server.api.scheduling.ISchedulingService;
import org.red5.server.api.stream.IBroadcastStream;
import org.red5.server.api.stream.IPlayItem;
import org.red5.server.api.stream.IVideoStreamCodec;
import org.red5.server.api.stream.OperationNotSupportedException;
import org.red5.server.messaging.AbstractMessage;
import org.red5.server.messaging.IFilter;
import org.red5.server.messaging.IMessage;
import org.red5.server.messaging.IMessageComponent;
import org.red5.server.messaging.IMessageInput;
import org.red5.server.messaging.IMessageOutput;
import org.red5.server.messaging.IPassive;
import org.red5.server.messaging.IPipe;
import org.red5.server.messaging.IPipeConnectionListener;
import org.red5.server.messaging.IProvider;
import org.red5.server.messaging.IPushableConsumer;
import org.red5.server.messaging.OOBControlMessage;
import org.red5.server.messaging.PipeConnectionEvent;
import org.red5.server.net.rtmp.event.AudioData;
import org.red5.server.net.rtmp.event.IRTMPEvent;
import org.red5.server.net.rtmp.event.Notify;
import org.red5.server.net.rtmp.event.Ping;
import org.red5.server.net.rtmp.event.VideoData;
import org.red5.server.net.rtmp.event.VideoData.FrameType;
import org.red5.server.net.rtmp.message.Header;
import org.red5.server.net.rtmp.status.Status;
import org.red5.server.net.rtmp.status.StatusCodes;
import org.red5.server.stream.AbstractStream.State;
import org.red5.server.stream.ITokenBucket.ITokenBucketCallback;
import org.red5.server.stream.message.RTMPMessage;
import org.red5.server.stream.message.ResetMessage;
import org.red5.server.stream.message.StatusMessage;
import org.slf4j.Logger;
/**
* A play engine for playing an IPlayItem.
*
* @author The Red5 Project (red5@osflash.org)
* @author Steven Gong
* @author Paul Gregoire (mondain@gmail.com)
*/
public final class PlayEngine implements IFilter, IPushableConsumer,
IPipeConnectionListener, ITokenBucketCallback {
private static final Logger log = Red5LoggerFactory.getLogger(PlayEngine.class);
private IMessageInput msgIn;
private IMessageOutput msgOut;
private final PlaylistSubscriberStream playlistSubscriberStream;
private ISchedulingService schedulingService;
private IConsumerService consumerService;
private IProviderService providerService;
/**
* Service that controls bandwidth
*/
private IBWControlService bwController;
/**
* Operating context for bandwidth controller
*/
private IBWControlContext bwContext;
private int streamId;
/**
* Receive video?
*/
private boolean receiveVideo = true;
/**
* Receive audio?
*/
private boolean receiveAudio = true;
private boolean pullMode;
private String waitLiveJob;
private boolean waiting;
/**
* timestamp of first sent packet
*/
private int streamStartTS;
private IPlayItem currentItem;
private ITokenBucket audioBucket;
private ITokenBucket videoBucket;
private RTMPMessage pendingMessage;
private boolean waitingForToken = false;
private boolean checkBandwidth = true;
/**
* Interval in ms to check for buffer underruns in VOD streams.
*/
private int bufferCheckInterval = 0;
/**
* Number of pending messages at which a <code>NetStream.Play.InsufficientBW</code>
* message is generated for VOD streams.
*/
private int underrunTrigger = 10;
/**
* State machine for video frame dropping in live streams
*/
private IFrameDropper videoFrameDropper = new VideoFrameDropper();
private int timestampOffset = 0;
/**
* Last message sent to the client.
*/
private IRTMPEvent lastMessage;
/**
* Number of bytes sent.
*/
private long bytesSent = 0;
/**
* Start time of stream playback.
* It's not a time when the stream is being played but
* the time when the stream should be played if it's played
* from the very beginning.
* Eg. A stream is played at timestamp 5s on 1:00:05. The
* playbackStart is 1:00:00.
*/
private long playbackStart;
/**
* Scheduled future job that makes sure messages are sent to the client.
*/
private volatile ScheduledFuture<?> pullAndPushFuture = null;
/**
* Offset in ms the stream started.
*/
private int streamOffset;
/**
* Timestamp when buffer should be checked for underruns next.
*/
private long nextCheckBufferUnderrun;
/**
* Send blank audio packet next?
*/
private boolean sendBlankAudio;
/**
* Constructs a new PlayEngine.
*/
private PlayEngine(Builder builder) {
playlistSubscriberStream = builder.playlistSubscriberStream;
schedulingService = builder.schedulingService;
consumerService = builder.consumerService;
providerService = builder.providerService;
//
streamId = playlistSubscriberStream.getStreamId();
}
/**
* Builder pattern
*/
public static class Builder {
//Required for play engine
private PlaylistSubscriberStream playlistSubscriberStream;
//Required for play engine
private ISchedulingService schedulingService;
//Required for play engine
private IConsumerService consumerService;
//Required for play engine
private IProviderService providerService;
public Builder(PlaylistSubscriberStream playlistSubscriberStream,
ISchedulingService schedulingService,
IConsumerService consumerService,
IProviderService providerService) {
this.playlistSubscriberStream = playlistSubscriberStream;
this.schedulingService = schedulingService;
this.consumerService = consumerService;
this.providerService = providerService;
}
public PlayEngine build() {
return new PlayEngine(this);
}
}
public void setBandwidthController(IBWControlService bwController,
IBWControlContext bwContext) {
this.bwController = bwController;
this.bwContext = bwContext;
}
public void setBufferCheckInterval(int bufferCheckInterval) {
this.bufferCheckInterval = bufferCheckInterval;
}
public void setUnderrunTrigger(int underrunTrigger) {
this.underrunTrigger = underrunTrigger;
}
void setMessageOut(IMessageOutput msgOut) {
this.msgOut = msgOut;
}
/**
* Start stream
*/
public synchronized void start() {
if (playlistSubscriberStream.state != State.UNINIT) {
throw new IllegalStateException();
}
playlistSubscriberStream.state = State.STOPPED;
if (msgOut == null) {
msgOut = consumerService.getConsumerOutput(playlistSubscriberStream);
msgOut.subscribe(this, null);
}
audioBucket = bwController.getAudioBucket(bwContext);
videoBucket = bwController.getVideoBucket(bwContext);
}
/**
* Play stream
* @param item Playlist item
* @throws StreamNotFoundException Stream not found
* @throws IllegalStateException Stream is in stopped state
* @throws IOException Stream had io exception
*/
public void play(IPlayItem item) throws StreamNotFoundException,
IllegalStateException, IOException {
play(item, true);
}
/**
* Play stream
* @param item Playlist item
* @param withReset Send reset status before playing.
* @throws StreamNotFoundException Stream not found
* @throws IllegalStateException Stream is in stopped state
* @throws IOException Stream had IO exception
*/
public synchronized void play(IPlayItem item, boolean withReset)
throws StreamNotFoundException, IllegalStateException, IOException {
// Can't play if state is not stopped
if (playlistSubscriberStream.state != State.STOPPED) {
throw new IllegalStateException();
}
if (msgIn != null) {
msgIn.unsubscribe(this);
msgIn = null;
}
// -2: live than recorded, -1: live, >=0: recorded
int type = (int) (item.getStart() / 1000);
// see if it's a published stream
IScope thisScope = playlistSubscriberStream.getScope();
String itemName = item.getName();
//check for input and type
IProviderService.INPUT_TYPE sourceType = providerService.lookupProviderInput(thisScope, itemName);
boolean isPublishedStream = sourceType == IProviderService.INPUT_TYPE.LIVE;
boolean isFileStream = sourceType == IProviderService.INPUT_TYPE.VOD;
boolean sendNotifications = true;
// decision: 0 for Live, 1 for File, 2 for Wait, 3 for N/A
int decision = 3;
switch (type) {
case -2:
if (isPublishedStream) {
decision = 0;
} else if (isFileStream) {
decision = 1;
} else {
decision = 2;
}
break;
case -1:
if (isPublishedStream) {
decision = 0;
} else {
decision = 2;
}
break;
default:
if (isFileStream) {
decision = 1;
}
break;
}
log.debug("play decision is {}", decision);
currentItem = item;
long itemLength = item.getLength();
switch (decision) {
case 0:
//get source input without create
msgIn = providerService.getLiveProviderInput(thisScope, itemName, false);
// Drop all frames up to the next keyframe
videoFrameDropper.reset(IFrameDropper.SEND_KEYFRAMES_CHECK);
if (msgIn instanceof IBroadcastScope) {
// Send initial keyframe
IBroadcastStream stream = (IBroadcastStream) ((IBroadcastScope) msgIn)
.getAttribute(IBroadcastScope.STREAM_ATTRIBUTE);
if (stream != null && stream.getCodecInfo() != null) {
IVideoStreamCodec videoCodec = stream.getCodecInfo()
.getVideoCodec();
if (videoCodec != null) {
ByteBuffer keyFrame = videoCodec.getKeyframe();
if (keyFrame != null) {
VideoData video = new VideoData(keyFrame);
try {
if (withReset) {
sendReset();
sendResetStatus(item);
sendStartStatus(item);
}
video.setTimestamp(0);
RTMPMessage videoMsg = new RTMPMessage();
videoMsg.setBody(video);
msgOut.pushMessage(videoMsg);
sendNotifications = false;
// Don't wait for keyframe
videoFrameDropper.reset();
} finally {
video.release();
}
}
}
}
}
msgIn.subscribe(this, null);
break;
case 2:
//get source input with create
msgIn = providerService.getLiveProviderInput(thisScope, itemName, true);
msgIn.subscribe(this, null);
waiting = true;
if (type == -1 && itemLength >= 0) {
// Wait given timeout for stream to be published
waitLiveJob = schedulingService.addScheduledOnceJob(
itemLength, new IScheduledJob() {
public void execute(ISchedulingService service) {
waitLiveJob = null;
waiting = false;
playlistSubscriberStream.onItemEnd();
}
});
}
break;
case 1:
msgIn = providerService.getVODProviderInput(thisScope, itemName);
if (msgIn == null) {
sendStreamNotFoundStatus(currentItem);
throw new StreamNotFoundException(itemName);
}
if (!msgIn.subscribe(this, null)) {
log.error("Input source subscribe failed");
}
break;
default:
sendStreamNotFoundStatus(currentItem);
throw new StreamNotFoundException(itemName);
}
playlistSubscriberStream.state = State.PLAYING;
IMessage msg = null;
streamOffset = 0;
streamStartTS = -1;
if (decision == 1) {
if (withReset) {
releasePendingMessage();
}
sendVODInitCM(msgIn, item);
// Don't use pullAndPush to detect IOExceptions prior to sending
// NetStream.Play.Start
if (item.getStart() > 0) {
streamOffset = sendVODSeekCM(msgIn, (int) item.getStart());
// We seeked to the nearest keyframe so use real timestamp now
if (streamOffset == -1) {
streamOffset = (int) item.getStart();
}
}
msg = msgIn.pullMessage();
if (msg instanceof RTMPMessage) {
IRTMPEvent body = ((RTMPMessage) msg).getBody();
if (itemLength == 0) {
// Only send first video frame
body = ((RTMPMessage) msg).getBody();
while (body != null && !(body instanceof VideoData)) {
msg = msgIn.pullMessage();
if (msg == null) {
break;
}
if (!(msg instanceof RTMPMessage)) {
continue;
}
body = ((RTMPMessage) msg).getBody();
}
}
if (body != null) {
// Adjust timestamp when playing lists
body.setTimestamp(body.getTimestamp() + timestampOffset);
}
}
}
if (sendNotifications) {
if (withReset) {
sendReset();
sendResetStatus(item);
}
sendStartStatus(item);
if (!withReset) {
sendSwitchStatus();
}
}
if (msg != null) {
sendMessage((RTMPMessage) msg);
}
playlistSubscriberStream.notifyItemPlay(currentItem, !pullMode);
if (withReset) {
long currentTime = System.currentTimeMillis();
playbackStart = currentTime - streamOffset;
nextCheckBufferUnderrun = currentTime + bufferCheckInterval;
if (currentItem.getLength() != 0) {
ensurePullAndPushRunning();
}
}
}
/**
* Pause at position
* @param position Position in file
* @throws IllegalStateException If stream is stopped
*/
public synchronized void pause(int position) throws IllegalStateException {
if ((playlistSubscriberStream.state != State.PLAYING && playlistSubscriberStream.state != State.STOPPED)
|| currentItem == null) {
throw new IllegalStateException();
}
playlistSubscriberStream.state = State.PAUSED;
releasePendingMessage();
clearWaitJobs();
sendClearPing();
sendPauseStatus(currentItem);
playlistSubscriberStream.notifyItemPause(currentItem, position);
}
/**
* Resume playback
* @param position Resumes playback
* @throws IllegalStateException If stream is stopped
*/
public synchronized void resume(int position) throws IllegalStateException {
if (playlistSubscriberStream.state != State.PAUSED) {
throw new IllegalStateException();
}
playlistSubscriberStream.state = State.PLAYING;
sendReset();
sendResumeStatus(currentItem);
if (pullMode) {
sendVODSeekCM(msgIn, position);
playlistSubscriberStream.notifyItemResume(currentItem, position);
playbackStart = System.currentTimeMillis() - position;
if (currentItem.getLength() >= 0
&& (position - streamOffset) >= currentItem.getLength()) {
// Resume after end of stream
stop();
} else {
ensurePullAndPushRunning();
}
} else {
playlistSubscriberStream.notifyItemResume(currentItem, position);
videoFrameDropper.reset(VideoFrameDropper.SEND_KEYFRAMES_CHECK);
}
}
/**
* Seek position in file
* @param position Position
* @throws IllegalStateException If stream is in stopped state
* @throws OperationNotSupportedException If this object doesn't support the operation.
*/
public synchronized void seek(int position) throws IllegalStateException,
OperationNotSupportedException {
if (playlistSubscriberStream.state != State.PLAYING
&& playlistSubscriberStream.state != State.PAUSED
&& playlistSubscriberStream.state != State.STOPPED) {
throw new IllegalStateException();
}
if (!pullMode) {
throw new OperationNotSupportedException();
}
releasePendingMessage();
clearWaitJobs();
bwController.resetBuckets(bwContext);
waitingForToken = false;
sendClearPing();
sendReset();
sendSeekStatus(currentItem, position);
sendStartStatus(currentItem);
int seekPos = sendVODSeekCM(msgIn, position);
// We seeked to the nearest keyframe so use real timestamp now
if (seekPos == -1) {
seekPos = position;
}
playbackStart = System.currentTimeMillis() - seekPos;
playlistSubscriberStream.notifyItemSeek(currentItem, seekPos);
boolean messageSent = false;
boolean startPullPushThread = false;
if ((playlistSubscriberStream.state == State.PAUSED
|| playlistSubscriberStream.state == State.STOPPED)
&& sendCheckVideoCM(msgIn)) {
// we send a single snapshot on pause.
// XXX we need to take BWC into account, for
// now send forcefully.
IMessage msg;
try {
msg = msgIn.pullMessage();
} catch (Throwable err) {
log.error("Error while pulling message", err);
msg = null;
}
while (msg != null) {
if (msg instanceof RTMPMessage) {
RTMPMessage rtmpMessage = (RTMPMessage) msg;
IRTMPEvent body = rtmpMessage.getBody();
if (body instanceof VideoData
&& ((VideoData) body).getFrameType() == FrameType.KEYFRAME) {
body.setTimestamp(seekPos);
doPushMessage(rtmpMessage);
rtmpMessage.getBody().release();
messageSent = true;
lastMessage = body;
break;
}
}
try {
msg = msgIn.pullMessage();
} catch (Throwable err) {
log.error("Error while pulling message", err);
msg = null;
}
}
} else {
startPullPushThread = true;
}
if (!messageSent) {
// Send blank audio packet to notify client about new position
AudioData audio = new AudioData();
audio.setTimestamp(seekPos);
audio.setHeader(new Header());
audio.getHeader().setTimer(seekPos);
audio.getHeader().setTimerRelative(false);
RTMPMessage audioMessage = new RTMPMessage();
audioMessage.setBody(audio);
lastMessage = audio;
doPushMessage(audioMessage);
}
if (startPullPushThread) {
ensurePullAndPushRunning();
}
if (playlistSubscriberStream.state != State.STOPPED && currentItem.getLength() >= 0
&& (position - streamOffset) >= currentItem.getLength()) {
// Seeked after end of stream
stop();
return;
}
}
/**
* Stop playback
* @throws IllegalStateException If stream is in stopped state
*/
public synchronized void stop() throws IllegalStateException {
if (playlistSubscriberStream.state != State.PLAYING
&& playlistSubscriberStream.state != State.PAUSED) {
throw new IllegalStateException();
}
playlistSubscriberStream.state = State.STOPPED;
if (msgIn != null && !pullMode) {
msgIn.unsubscribe(this);
msgIn = null;
}
playlistSubscriberStream.notifyItemStop(currentItem);
clearWaitJobs();
if (!playlistSubscriberStream.hasMoreItems()) {
releasePendingMessage();
bwController.resetBuckets(bwContext);
waitingForToken = false;
if (playlistSubscriberStream.getItemSize() > 0) {
sendCompleteStatus();
}
bytesSent = 0;
sendClearPing();
sendStopStatus(currentItem);
} else {
if (lastMessage != null) {
// Remember last timestamp so we can generate correct
// headers in playlists.
timestampOffset = lastMessage.getTimestamp();
}
playlistSubscriberStream.nextItem();
}
}
/**
* Close stream
*/
public synchronized void close() {
if (msgIn != null) {
msgIn.unsubscribe(this);
msgIn = null;
}
playlistSubscriberStream.state = State.CLOSED;
clearWaitJobs();
releasePendingMessage();
lastMessage = null;
sendClearPing();
}
/**
* Check if it's okay to send the client more data. This takes the configured
* bandwidth as well as the requested client buffer into account.
*
* @param message
* @return
*/
private boolean okayToSendMessage(IRTMPEvent message) {
if (!(message instanceof IStreamData)) {
String itemName = "Undefined";
//if current item exists get the name to help debug this issue
if (currentItem != null) {
itemName = currentItem.getName();
}
Object[] errorItems = new Object[]{message.getClass(), message.getDataType(), itemName};
throw new RuntimeException(String.format("Expected IStreamData but got %s (type %s) for %s", errorItems));
}
final long now = System.currentTimeMillis();
// check client buffer length when we've already sent some messages
if (lastMessage != null) {
// Duration the stream is playing
final long delta = now - playbackStart;
// Buffer size as requested by the client
final long buffer = playlistSubscriberStream.getClientBufferDuration();
// Expected amount of data present in client buffer
final long buffered = lastMessage.getTimestamp() - delta;
log
.debug(
"okayToSendMessage: timestamp {} delta {} buffered {} buffer {}",
new Object[] { lastMessage.getTimestamp(), delta,
buffered, buffer });
if (buffer > 0 && buffered > buffer) {
// Client is likely to have enough data in the buffer
return false;
}
}
long pending = pendingMessages();
if (bufferCheckInterval > 0 && now >= nextCheckBufferUnderrun) {
if (pending > underrunTrigger) {
// Client is playing behind speed, notify him
sendInsufficientBandwidthStatus(currentItem);
}
nextCheckBufferUnderrun = now + bufferCheckInterval;
}
if (pending > underrunTrigger) {
// Too many messages already queued on the connection
return false;
}
if (((IStreamData) message).getData() == null) {
// TODO: when can this happen?
return true;
}
final int size = ((IStreamData) message).getData().limit();
if (message instanceof VideoData) {
if (checkBandwidth
&& !videoBucket.acquireTokenNonblocking(size, this)) {
waitingForToken = true;
return false;
}
} else if (message instanceof AudioData) {
if (checkBandwidth
&& !audioBucket.acquireTokenNonblocking(size, this)) {
waitingForToken = true;
return false;
}
}
return true;
}
/**
* Make sure the pull and push processing is running.
*/
private void ensurePullAndPushRunning() {
if (!pullMode) {
// We don't need this for live streams
return;
}
if (pullAndPushFuture == null) {
synchronized (this) {
if (pullAndPushFuture == null) {
// client buffer is at least 100ms
pullAndPushFuture = playlistSubscriberStream.getExecutor().scheduleWithFixedDelay(
new PullAndPushRunnable(), 0, 10,
TimeUnit.MILLISECONDS);
}
}
}
}
/**
* Recieve then send if message is data (not audio or video)
*/
protected synchronized void pullAndPush() throws IOException {
if (playlistSubscriberStream.state == State.PLAYING && pullMode && !waitingForToken) {
if (pendingMessage != null) {
IRTMPEvent body = pendingMessage.getBody();
if (!okayToSendMessage(body)) {
return;
}
sendMessage(pendingMessage);
releasePendingMessage();
} else {
while (true) {
IMessage msg = msgIn.pullMessage();
if (msg == null) {
// No more packets to send
stop();
break;
} else {
if (msg instanceof RTMPMessage) {
RTMPMessage rtmpMessage = (RTMPMessage) msg;
IRTMPEvent body = rtmpMessage.getBody();
if (!receiveAudio && body instanceof AudioData) {
// The user doesn't want to get audio packets
((IStreamData) body).getData().release();
if (sendBlankAudio) {
// Send reset audio packet
sendBlankAudio = false;
body = new AudioData();
// We need a zero timestamp
if (lastMessage != null) {
body.setTimestamp(lastMessage
.getTimestamp()
- timestampOffset);
} else {
body.setTimestamp(-timestampOffset);
}
rtmpMessage.setBody(body);
} else {
continue;
}
} else if (!receiveVideo
&& body instanceof VideoData) {
// The user doesn't want to get video packets
((IStreamData) body).getData().release();
continue;
}
// Adjust timestamp when playing lists
body.setTimestamp(body.getTimestamp()
+ timestampOffset);
if (okayToSendMessage(body)) {
//System.err.println("ts: " + rtmpMessage.getBody().getTimestamp());
sendMessage(rtmpMessage);
((IStreamData) body).getData().release();
} else {
pendingMessage = rtmpMessage;
}
ensurePullAndPushRunning();
break;
}
}
}
}
}
}
/**
* Clear all scheduled waiting jobs
*/
private void clearWaitJobs() {
if (pullAndPushFuture != null) {
pullAndPushFuture.cancel(false);
pullAndPushFuture = null;
}
if (waitLiveJob != null) {
schedulingService.removeScheduledJob(waitLiveJob);
waitLiveJob = null;
}
}
/**
* Send message to output stream and handle exceptions.
*
* @param message The message to send.
*/
private void doPushMessage(AbstractMessage message) {
try {
msgOut.pushMessage(message);
if (message instanceof RTMPMessage) {
IRTMPEvent body = ((RTMPMessage) message).getBody();
if (body instanceof IStreamData
&& ((IStreamData) body).getData() != null) {
bytesSent += ((IStreamData) body).getData().limit();
}
}
} catch (IOException err) {
log.error("Error while pushing message.", err);
}
}
/**
* Send RTMP message
* @param message RTMP message
*/
private void sendMessage(RTMPMessage message) {
log.debug("sendMessage: streamStartTS={}, length={}, streamOffset={}, timestamp={}",
new Object[]{streamStartTS, currentItem.getLength(), streamOffset,
message.getBody().getTimestamp()});
if (streamStartTS == -1) {
log.debug("sendMessage: resetting streamStartTS");
streamStartTS = message.getBody().getTimestamp();
} else {
if (currentItem.getLength() >= 0) {
int duration = message.getBody().getTimestamp() - streamStartTS;
if (duration - streamOffset >= currentItem.getLength()) {
// Sent enough data to client
stop();
return;
}
}
}
lastMessage = message.getBody();
if (lastMessage instanceof IStreamData
&& ((IStreamData) lastMessage).getData() != null) {
bytesSent += ((IStreamData) lastMessage).getData().limit();
}
doPushMessage(message);
}
/**
* Send clear ping, that is, just to check if connection is alive
*/
private void sendClearPing() {
Ping ping1 = new Ping();
ping1.setValue1((short) Ping.STREAM_PLAYBUFFER_CLEAR);
ping1.setValue2(streamId);
RTMPMessage ping1Msg = new RTMPMessage();
ping1Msg.setBody(ping1);
doPushMessage(ping1Msg);
}
/**
* Send reset message
*/
private void sendReset() {
if (pullMode) {
Ping ping1 = new Ping();
ping1.setValue1((short) Ping.STREAM_RESET);
ping1.setValue2(streamId);
RTMPMessage ping1Msg = new RTMPMessage();
ping1Msg.setBody(ping1);
doPushMessage(ping1Msg);
}
Ping ping2 = new Ping();
ping2.setValue1((short) Ping.STREAM_CLEAR);
ping2.setValue2(streamId);
RTMPMessage ping2Msg = new RTMPMessage();
ping2Msg.setBody(ping2);
doPushMessage(ping2Msg);
ResetMessage reset = new ResetMessage();
doPushMessage(reset);
}
/**
* Send reset status for item
* @param item Playlist item
*/
private void sendResetStatus(IPlayItem item) {
Status reset = new Status(StatusCodes.NS_PLAY_RESET);
reset.setClientid(streamId);
reset.setDetails(item.getName());
reset.setDesciption(String.format("Playing and resetting %s.", item.getName()));
StatusMessage resetMsg = new StatusMessage();
resetMsg.setBody(reset);
doPushMessage(resetMsg);
}
/**
* Send playback start status notification
* @param item Playlist item
*/
private void sendStartStatus(IPlayItem item) {
Status start = new Status(StatusCodes.NS_PLAY_START);
start.setClientid(streamId);
start.setDetails(item.getName());
start.setDesciption(String.format("Started playing %s.", item.getName()));
StatusMessage startMsg = new StatusMessage();
startMsg.setBody(start);
doPushMessage(startMsg);
}
/**
* Send playback stoppage status notification
* @param item Playlist item
*/
private void sendStopStatus(IPlayItem item) {
Status stop = new Status(StatusCodes.NS_PLAY_STOP);
stop.setClientid(streamId);
stop.setDesciption(String.format("Stopped playing %s.", item.getName()));
stop.setDetails(item.getName());
StatusMessage stopMsg = new StatusMessage();
stopMsg.setBody(stop);
doPushMessage(stopMsg);
}
private void sendOnPlayStatus(String code, int duration, long bytes) {
ByteBuffer buf = ByteBuffer.allocate(1024);
buf.setAutoExpand(true);
Output out = new Output(buf);
out.writeString("onPlayStatus");
Map<Object, Object> props = new HashMap<Object, Object>();
props.put("code", code);
props.put("level", "status");
props.put("duration", duration);
props.put("bytes", bytes);
out.writeMap(props, new Serializer());
buf.flip();
IRTMPEvent event = new Notify(buf);
if (lastMessage != null) {
int timestamp = lastMessage.getTimestamp();
event.setTimestamp(timestamp);
} else {
event.setTimestamp(0);
}
RTMPMessage msg = new RTMPMessage();
msg.setBody(event);
doPushMessage(msg);
}
/**
* Send playlist switch status notification
*/
private void sendSwitchStatus() {
// TODO: find correct duration to sent
int duration = 1;
sendOnPlayStatus(StatusCodes.NS_PLAY_SWITCH, duration, bytesSent);
}
/**
* Send playlist complete status notification
*
*/
private void sendCompleteStatus() {
// TODO: find correct duration to sent
int duration = 1;
sendOnPlayStatus(StatusCodes.NS_PLAY_COMPLETE, duration, bytesSent);
}
/**
* Send seek status notification
* @param item Playlist item
* @param position Seek position
*/
private void sendSeekStatus(IPlayItem item, int position) {
Status seek = new Status(StatusCodes.NS_SEEK_NOTIFY);
seek.setClientid(streamId);
seek.setDetails(item.getName());
seek.setDesciption(String.format("Seeking %d (stream ID: %d).", position, streamId));
StatusMessage seekMsg = new StatusMessage();
seekMsg.setBody(seek);
doPushMessage(seekMsg);
}
/**
* Send pause status notification
* @param item Playlist item
*/
private void sendPauseStatus(IPlayItem item) {
Status pause = new Status(StatusCodes.NS_PAUSE_NOTIFY);
pause.setClientid(streamId);
pause.setDetails(item.getName());
StatusMessage pauseMsg = new StatusMessage();
pauseMsg.setBody(pause);
doPushMessage(pauseMsg);
}
/**
* Send resume status notification
* @param item Playlist item
*/
private void sendResumeStatus(IPlayItem item) {
Status resume = new Status(StatusCodes.NS_UNPAUSE_NOTIFY);
resume.setClientid(streamId);
resume.setDetails(item.getName());
StatusMessage resumeMsg = new StatusMessage();
resumeMsg.setBody(resume);
doPushMessage(resumeMsg);
}
/**
* Send published status notification
* @param item Playlist item
*/
private void sendPublishedStatus(IPlayItem item) {
Status published = new Status(StatusCodes.NS_PLAY_PUBLISHNOTIFY);
published.setClientid(streamId);
published.setDetails(item.getName());
StatusMessage unpublishedMsg = new StatusMessage();
unpublishedMsg.setBody(published);
doPushMessage(unpublishedMsg);
}
/**
* Send unpublished status notification
* @param item Playlist item
*/
private void sendUnpublishedStatus(IPlayItem item) {
Status unpublished = new Status(StatusCodes.NS_PLAY_UNPUBLISHNOTIFY);
unpublished.setClientid(streamId);
unpublished.setDetails(item.getName());
StatusMessage unpublishedMsg = new StatusMessage();
unpublishedMsg.setBody(unpublished);
doPushMessage(unpublishedMsg);
}
/**
* Stream not found status notification
* @param item Playlist item
*/
private void sendStreamNotFoundStatus(IPlayItem item) {
Status notFound = new Status(StatusCodes.NS_PLAY_STREAMNOTFOUND);
notFound.setClientid(streamId);
notFound.setLevel(Status.ERROR);
notFound.setDetails(item.getName());
StatusMessage notFoundMsg = new StatusMessage();
notFoundMsg.setBody(notFound);
doPushMessage(notFoundMsg);
}
/**
* Insufficient bandwidth notification
* @param item Playlist item
*/
private void sendInsufficientBandwidthStatus(IPlayItem item) {
Status insufficientBW = new Status(StatusCodes.NS_PLAY_INSUFFICIENT_BW);
insufficientBW.setClientid(streamId);
insufficientBW.setLevel(Status.WARNING);
insufficientBW.setDetails(item.getName());
insufficientBW
.setDesciption("Data is playing behind the normal speed.");
StatusMessage insufficientBWMsg = new StatusMessage();
insufficientBWMsg.setBody(insufficientBW);
doPushMessage(insufficientBWMsg);
}
/**
* Send VOD init control message
* @param msgIn Message input
* @param item Playlist item
*/
private void sendVODInitCM(IMessageInput msgIn, IPlayItem item) {
OOBControlMessage oobCtrlMsg = new OOBControlMessage();
oobCtrlMsg.setTarget(IPassive.KEY);
oobCtrlMsg.setServiceName("init");
Map<String, Object> paramMap = new HashMap<String, Object>();
paramMap.put("startTS", (int) item.getStart());
oobCtrlMsg.setServiceParamMap(paramMap);
msgIn.sendOOBControlMessage(this, oobCtrlMsg);
}
/**
* Send VOD seek control message
* @param msgIn Message input
* @param position Playlist item
* @return Out-of-band control message call result or -1 on failure
*/
private int sendVODSeekCM(IMessageInput msgIn, int position) {
OOBControlMessage oobCtrlMsg = new OOBControlMessage();
oobCtrlMsg.setTarget(ISeekableProvider.KEY);
oobCtrlMsg.setServiceName("seek");
Map<String, Object> paramMap = new HashMap<String, Object>();
paramMap.put("position", position);
oobCtrlMsg.setServiceParamMap(paramMap);
msgIn.sendOOBControlMessage(this, oobCtrlMsg);
if (oobCtrlMsg.getResult() instanceof Integer) {
return (Integer) oobCtrlMsg.getResult();
} else {
return -1;
}
}
/**
* Send VOD check video control message
*
* @param msgIn
* @return
*/
private boolean sendCheckVideoCM(IMessageInput msgIn) {
OOBControlMessage oobCtrlMsg = new OOBControlMessage();
oobCtrlMsg.setTarget(IStreamTypeAwareProvider.KEY);
oobCtrlMsg.setServiceName("hasVideo");
msgIn.sendOOBControlMessage(this, oobCtrlMsg);
if (oobCtrlMsg.getResult() instanceof Boolean) {
return (Boolean) oobCtrlMsg.getResult();
} else {
return false;
}
}
/** {@inheritDoc} */
public void onOOBControlMessage(IMessageComponent source, IPipe pipe,
OOBControlMessage oobCtrlMsg) {
if ("ConnectionConsumer".equals(oobCtrlMsg.getTarget())) {
if (source instanceof IProvider) {
msgOut.sendOOBControlMessage((IProvider) source, oobCtrlMsg);
}
}
}
/** {@inheritDoc} */
public void onPipeConnectionEvent(PipeConnectionEvent event) {
switch (event.getType()) {
case PipeConnectionEvent.PROVIDER_CONNECT_PUSH:
if (event.getProvider() != this) {
if (waiting) {
schedulingService.removeScheduledJob(waitLiveJob);
waitLiveJob = null;
waiting = false;
}
sendPublishedStatus(currentItem);
}
break;
case PipeConnectionEvent.PROVIDER_DISCONNECT:
if (pullMode) {
sendStopStatus(currentItem);
} else {
sendUnpublishedStatus(currentItem);
}
break;
case PipeConnectionEvent.CONSUMER_CONNECT_PULL:
if (event.getConsumer() == this) {
pullMode = true;
}
break;
case PipeConnectionEvent.CONSUMER_CONNECT_PUSH:
if (event.getConsumer() == this) {
pullMode = false;
}
break;
default:
break;
}
}
/** {@inheritDoc} */
public synchronized void pushMessage(IPipe pipe, IMessage message)
throws IOException {
if (message instanceof ResetMessage) {
sendReset();
return;
}
if (message instanceof RTMPMessage) {
RTMPMessage rtmpMessage = (RTMPMessage) message;
IRTMPEvent body = rtmpMessage.getBody();
if (!(body instanceof IStreamData)) {
throw new RuntimeException(String.format("Expected IStreamData but got %s (type %s)", body.getClass(), body.getDataType()));
}
int size = ((IStreamData) body).getData().limit();
if (body instanceof VideoData) {
IVideoStreamCodec videoCodec = null;
if (msgIn instanceof IBroadcastScope) {
IBroadcastStream stream = (IBroadcastStream) ((IBroadcastScope) msgIn)
.getAttribute(IBroadcastScope.STREAM_ATTRIBUTE);
if (stream != null && stream.getCodecInfo() != null) {
videoCodec = stream.getCodecInfo().getVideoCodec();
}
}
//dont try to drop frames if video codec is null - related to SN-77
if (videoCodec != null && videoCodec.canDropFrames()) {
if (playlistSubscriberStream.state == State.PAUSED) {
// The subscriber paused the video
videoFrameDropper.dropPacket(rtmpMessage);
return;
}
// Only check for frame dropping if the codec supports it
long pendingVideos = pendingVideoMessages();
if (!videoFrameDropper.canSendPacket(rtmpMessage,
pendingVideos)) {
// Drop frame as it depends on other frames that were dropped before.
return;
}
boolean drop = !videoBucket.acquireToken(size, 0);
if (!receiveVideo || drop) {
// The client disabled video or the app doesn't have enough bandwidth
// allowed for this stream.
videoFrameDropper.dropPacket(rtmpMessage);
return;
}
if (pendingVideos > 1) {
// We drop because the client has insufficient bandwidth.
long now = System.currentTimeMillis();
if (bufferCheckInterval > 0
&& now >= nextCheckBufferUnderrun) {
// Notify client about frame dropping (keyframe)
sendInsufficientBandwidthStatus(currentItem);
nextCheckBufferUnderrun = now + bufferCheckInterval;
}
videoFrameDropper.dropPacket(rtmpMessage);
return;
}
videoFrameDropper.sendPacket(rtmpMessage);
}
} else if (body instanceof AudioData) {
if (!receiveAudio && sendBlankAudio) {
// Send blank audio packet to reset player
sendBlankAudio = false;
body = new AudioData();
if (lastMessage != null) {
body.setTimestamp(lastMessage.getTimestamp());
} else {
body.setTimestamp(0);
}
rtmpMessage.setBody(body);
} else if (playlistSubscriberStream.state == State.PAUSED
|| !receiveAudio
|| !audioBucket.acquireToken(size, 0)) {
return;
}
}
sendMessage(rtmpMessage);
} else {
msgOut.pushMessage(message);
}
}
/** {@inheritDoc} */
public synchronized void available(ITokenBucket bucket, long tokenCount) {
waitingForToken = false;
checkBandwidth = false;
try {
pullAndPush();
} catch (Throwable err) {
log.error("Error while pulling message.", err);
}
checkBandwidth = true;
}
/** {@inheritDoc} */
public void reset(ITokenBucket bucket, long tokenCount) {
waitingForToken = false;
}
/**
* Update bandwidth configuration
*/
public void updateBandwithConfigure() {
bwController.updateBWConfigure(bwContext);
}
/**
* Get number of pending video messages
* @return Number of pending video messages
*/
private long pendingVideoMessages() {
OOBControlMessage pendingRequest = new OOBControlMessage();
pendingRequest.setTarget("ConnectionConsumer");
pendingRequest.setServiceName("pendingVideoCount");
msgOut.sendOOBControlMessage(this, pendingRequest);
if (pendingRequest.getResult() != null) {
return (Long) pendingRequest.getResult();
} else {
return 0;
}
}
/**
* Get number of pending messages to be sent
* @return Number of pending messages
*/
private long pendingMessages() {
return playlistSubscriberStream.getConnection().getPendingMessages();
}
public boolean isPullMode() {
return pullMode;
}
public boolean isPaused() {
return playlistSubscriberStream.state == State.PAUSED;
}
public IRTMPEvent getLastMessage() {
return lastMessage;
}
public long getPlaybackStart() {
return playbackStart;
}
public void sendBlankAudio(boolean sendBlankAudio) {
this.sendBlankAudio = sendBlankAudio;
}
/**
* Returns true if the engine currently receives audio.
*
* @return engine receives audio
*/
public boolean receiveAudio() {
return receiveAudio;
}
/**
* Returns true if the engine currently receives audio and
* sets the new value.
*
* @param receive new value
* @return old value
*/
public boolean receiveAudio(boolean receive) {
boolean oldValue = receiveAudio;
//set new value
if (receiveAudio != receive) {
receiveAudio = receive;
}
return oldValue;
}
/**
* Returns true if the engine currently receives video.
*
* @return receive video
*/
public boolean receiveVideo() {
return receiveVideo;
}
/**
* Returns true if the engine currently receives video and
* sets the new value.
* @param receive new value
* @return old value
*/
public boolean receiveVideo(boolean receive) {
boolean oldValue = receiveVideo;
//set new value
if (receiveVideo != receive) {
receiveVideo = receive;
}
return oldValue;
}
/**
* Releases pending message body, nullifies pending message object
*/
private synchronized void releasePendingMessage() {
if (pendingMessage != null) {
IRTMPEvent body = pendingMessage.getBody();
if (body instanceof IStreamData
&& ((IStreamData) body).getData() != null) {
((IStreamData) body).getData().release();
}
pendingMessage.setBody(null);
pendingMessage = null;
}
}
/**
* Periodically triggered by executor to send messages to the client.
*/
private class PullAndPushRunnable implements Runnable {
/**
* Trigger sending of messages.
*/
public void run() {
try {
pullAndPush();
} catch (IOException err) {
// We couldn't get more data, stop stream.
log.error("Error while getting message", err);
PlayEngine.this.stop();
}
}
}
}