/*
* Flazr <http://flazr.com> Copyright (C) 2009 Peter Thomas.
*
* This file is part of Flazr.
*
* Flazr 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 3 of the License, or
* (at your option) any later version.
*
* Flazr 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 Flazr. If not, see <http://www.gnu.org/licenses/>.
*/
package com.flazr.rtmp;
import java.util.concurrent.TimeUnit;
import org.jboss.netty.channel.Channel;
import org.jboss.netty.channel.ChannelFuture;
import org.jboss.netty.channel.ChannelFutureListener;
import org.jboss.netty.channel.Channels;
import org.jboss.netty.channel.MessageEvent;
import org.jboss.netty.util.HashedWheelTimer;
import org.jboss.netty.util.Timeout;
import org.jboss.netty.util.Timer;
import org.jboss.netty.util.TimerTask;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import com.flazr.io.f4v.F4vReader;
import com.flazr.io.flv.FlvReader;
import com.flazr.rtmp.server.RtmpServer;
public abstract class RtmpPublisher {
private static final Logger logger = LoggerFactory.getLogger(RtmpPublisher.class);
private final Timer timer;
private final int timerTickSize;
private final boolean usingSharedTimer;
private final boolean aggregateModeEnabled;
private final RtmpReader reader;
private int streamId;
private long startTime;
private long seekTime;
private long timePosition;
private int currentConversationId;
private int playLength = -1;
private boolean paused;
private int bufferDuration;
public Channel channel;
private int channelId = 8;
public static class Event {
private final int conversationId;
private final int streamId;
public Event(final int conversationId, final int streamId) {
this.conversationId = conversationId;
this.streamId = streamId;
}
public int getConversationId() {
return conversationId;
}
public int getStreamId() {
return streamId;
}
}
public RtmpPublisher(final RtmpReader reader, final int streamId, final int bufferDuration,
boolean useSharedTimer, boolean aggregateModeEnabled) {
this.aggregateModeEnabled = aggregateModeEnabled;
this.usingSharedTimer = useSharedTimer;
if(useSharedTimer) {
timer = RtmpServer.TIMER;
} else {
timer = new HashedWheelTimer(RtmpConfig.TIMER_TICK_SIZE, TimeUnit.MILLISECONDS);
}
timerTickSize = RtmpConfig.TIMER_TICK_SIZE;
this.reader = reader;
this.streamId = streamId;
this.bufferDuration = bufferDuration;
logger.debug("publisher init, streamId: {}", streamId);
}
public static RtmpReader getReader(String path) {
if(path.toLowerCase().startsWith("mp4:")) {
return new F4vReader(path.substring(4));
} else if (path.toLowerCase().endsWith(".f4v")) {
return new F4vReader(path);
} else {
return new FlvReader(path);
}
}
public boolean isStarted() {
return currentConversationId > 0;
}
public boolean isPaused() {
return paused;
}
public void setBufferDuration(int bufferDuration) {
this.bufferDuration = bufferDuration;
}
public boolean handle(final MessageEvent me) {
if(me.getMessage() instanceof Event) {
final Event pe = (Event) me.getMessage();
if(pe.streamId != streamId) {
return false;
}
if(pe.conversationId != currentConversationId) {
logger.debug("stopping obsolete conversation id: {}, current: {}",
pe.getConversationId(), currentConversationId);
return true;
}
write(me.getChannel());
return true;
}
return false;
}
public void start(final Channel channel, final int seekTime, final int playLength, final RtmpMessage ... messages) {
this.channel = channel;
this.playLength = playLength;
start(channel, seekTime, messages);
}
public void start(final Channel channel, final int seekTimeRequested, final RtmpMessage ... messages) {
paused = false;
currentConversationId++;
startTime = System.currentTimeMillis();
if(seekTimeRequested >= 0) {
seekTime = reader.seek(seekTimeRequested);
} else {
seekTime = 0;
}
timePosition = seekTime;
logger.debug("publish start, seek requested: {} actual seek: {}, play length: {}, conversation: {}",
new Object[]{seekTimeRequested, seekTime, playLength, currentConversationId});
for(final RtmpMessage message : messages) {
writeToStream(channel, message);
}
for(final RtmpMessage message : reader.getStartMessages()) {
writeToStream(channel, message);
}
write(channel);
}
public void writeToStream(final Channel channel, final RtmpMessage message) {
if(message.getHeader().getChannelId() > 2) {
message.getHeader().setStreamId(streamId);
message.getHeader().setTime((int) timePosition);
}
channel.write(message);
}
private void write(final Channel channel) {
if(!channel.isWritable()) {
return;
}
final long writeTime = System.currentTimeMillis();
final RtmpMessage message;
synchronized(reader) { //=============== SYNCHRONIZE ! =================
if(reader.hasNext()) {
message = reader.next();
} else {
message = null;
}
} //====================================================================
if (message == null || playLength >= 0 && timePosition > (seekTime + playLength)) {
stop(channel);
return;
}
final long elapsedTime = System.currentTimeMillis() - startTime;
final long elapsedTimePlusSeek = elapsedTime + seekTime;
final double clientBuffer = timePosition - elapsedTimePlusSeek;
if(aggregateModeEnabled && clientBuffer > timerTickSize) { // TODO cleanup
reader.setAggregateDuration((int) clientBuffer);
} else {
reader.setAggregateDuration(0);
}
final RtmpHeader header = message.getHeader();
final double compensationFactor = clientBuffer / (bufferDuration + timerTickSize);
final long delay = (long) ((header.getTime() - timePosition) * compensationFactor);
if(logger.isDebugEnabled()) {
logger.debug("elapsed: {}, streamed: {}, buffer: {}, factor: {}, delay: {}",
new Object[]{elapsedTimePlusSeek, timePosition, clientBuffer, compensationFactor, delay});
}
timePosition = header.getTime();
header.setStreamId(streamId);
header.setChannelId(channelId);
final ChannelFuture future = channel.write(message);
future.addListener(new ChannelFutureListener() {
@Override public void operationComplete(final ChannelFuture cf) {
final long completedIn = System.currentTimeMillis() - writeTime;
if(completedIn > 2000) {
logger.warn("channel busy? time taken to write last message: {}", completedIn);
}
final long delayToUse = clientBuffer > 0 ? delay - completedIn : 0;
fireNext(channel, delayToUse);
}
});
}
public void fireNext(final Channel channel, final long delay) {
final Event readyForNext = new Event(currentConversationId, streamId);
if(delay > timerTickSize) {
timer.newTimeout(new TimerTask() {
@Override public void run(Timeout timeout) {
if(logger.isDebugEnabled()) {
logger.debug("running after delay: {}", delay);
}
if(readyForNext.conversationId != currentConversationId) {
logger.debug("pending 'next' event found obsolete, aborting");
return;
}
Channels.fireMessageReceived(channel, readyForNext);
}
}, delay, TimeUnit.MILLISECONDS);
} else {
Channels.fireMessageReceived(channel, readyForNext);
}
}
public void pause() {
paused = true;
currentConversationId++;
}
private void stop(final Channel channel) {
currentConversationId++;
final long elapsedTime = System.currentTimeMillis() - startTime;
logger.info("finished, start: {}, elapsed {}, streamed: {}",
new Object[]{seekTime / 1000, elapsedTime / 1000, (timePosition - seekTime) / 1000});
for(RtmpMessage message : getStopMessages(timePosition)) {
writeToStream(channel, message);
}
}
public void close() {
if(!usingSharedTimer) {
timer.stop();
}
reader.close();
}
protected abstract RtmpMessage[] getStopMessages(long timePosition);
public void setChannelId(int channelId) {
this.channelId = channelId;
}
}