/*
* myLib - https://github.com/taktod/myLib
* Copyright (c) 2014 ttProject. All rights reserved.
*
* Licensed under GNU LESSER GENERAL PUBLIC LICENSE Version 3.
*/
package com.ttProject.flazr.test.publish;
import java.net.InetSocketAddress;
import java.util.concurrent.Executor;
import java.util.concurrent.Executors;
import java.util.concurrent.LinkedBlockingQueue;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import org.jboss.netty.bootstrap.ClientBootstrap;
import org.jboss.netty.channel.ChannelFactory;
import org.jboss.netty.channel.ChannelFuture;
import org.jboss.netty.channel.ChannelPipeline;
import org.jboss.netty.channel.ChannelPipelineFactory;
import org.jboss.netty.channel.Channels;
import org.jboss.netty.channel.socket.nio.NioClientSocketChannelFactory;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import com.flazr.io.flv.FlvAtom;
import com.flazr.rtmp.RtmpDecoder;
import com.flazr.rtmp.RtmpMessage;
import com.flazr.rtmp.RtmpReader;
import com.flazr.rtmp.client.ClientHandshakeHandler;
import com.flazr.rtmp.client.ClientOptions;
import com.flazr.rtmp.message.Metadata;
import com.flazr.rtmp.message.MetadataAmf0;
import com.ttProject.container.flv.FlvCodecType;
import com.ttProject.container.flv.FlvTag;
import com.ttProject.container.flv.type.AudioTag;
import com.ttProject.container.flv.type.VideoTag;
import com.ttProject.flazr.rtmp.RtmpEncoderEx;
import com.ttProject.flazr.unit.TagManager;
/**
* rtmpの送信を実行する動作
* 受け取る側がwriterなので、送り出す側がreader(flvファイルデータを読み込んで流すみたいな感じですね。きっと)
* @author taktod
*/
public class SendReader implements RtmpReader {
/** ロガー */
private final Logger logger = LoggerFactory.getLogger(SendReader.class);
/** 配信用のthreadに渡すデータの中継役 */
private final LinkedBlockingQueue<FlvAtom> dataQueue = new LinkedBlockingQueue<FlvAtom>();
/** デフォルトのメタデータ */
private Metadata metadata = new MetadataAmf0("onMetaData");
/** 集合メッセージの設定保持(常に0を期待します) */
private int aggregateDuration = 0;
/** 接続サーバーアドレス */
private final String rtmpAddress;
/** アドレス解析用(正規表現) */
private static final Pattern pattern = Pattern.compile("^rtmp://([^/:]+)(:[0-9]+)?/(.*)(.*?)$");
/** 接続処理用 */
private ClientBootstrap bootstrap = null;
private ChannelFuture future = null;
private ClientOptions options;
private SendClientHandler clientHandler = null;
/** 接続中管理フラグ */
private boolean isWorking = true;
/** 放送中管理フラグ */
private boolean isPublishing = true;
/** flvの特別なデータ管理用 */
private AudioTag audioMshTag = null;
private VideoTag videoMshTag = null;
/** dataQueue上の処理位置 */
// private int processPos = -1;
/** 現在の処理位置 */
// private int savePos = 0;
/** FlvTag -> flvAtomの変換補助 */
private final TagManager manager = new TagManager();
/**
* コンストラクタ
* @param rtmpAddress 接続先アドレス
*/
public SendReader(String rtmpAddress) {
this.rtmpAddress = rtmpAddress;
}
/**
* 接続を開きます。
* @throws Exception
*/
public void open() throws Exception {
options = new ClientOptions();
parseAddress(options);
options.publishLive();
options.setFileToPublish(null);
options.setReaderToPublish(this);
options.setStreamName("test");
logger.info("open connection");
// 接続を開始します。blockするので、threadにやらせます。
Thread t = new Thread(new Runnable() {
@Override
public void run() {
while(isWorking) {
try {
dataQueue.clear();
audioMshTag = null;
videoMshTag = null;
// processPos = -1;
// savePos = 0;
// このタイミングで前のmshデータを要求してやる必要があるっぽいです。
}
catch(Exception e) {
}
connect(options);
logger.info("connect success.");
}
logger.info("thread of rtmpPublish is ended.");
// なにか停止処理をいれる場合はここにいれるべし。
}
});
// プロセスが落ちたら自動的におわりたいので、daemon化しておきます。
t.setDaemon(true);
t.start();
}
/**
* 接続処理
* @param options
*/
private void connect(final ClientOptions options) {
bootstrap = getBootstrap(Executors.newCachedThreadPool(), options);
future = bootstrap.connect(new InetSocketAddress(options.getHost(), options.getPort()));
future.awaitUninterruptibly();
if(!future.isSuccess()) {
logger.warn("failed to connect");
}
else {
logger.info("success to connect");
}
// これやっちゃうと・・・他の処理がしにそうな気がするけど・・・
future.getChannel().getCloseFuture().awaitUninterruptibly();
bootstrap.getFactory().releaseExternalResources();
}
/**
* bootstrapの作成処理
* @param executor
* @param options
* @return
*/
private ClientBootstrap getBootstrap(final Executor executor, final ClientOptions options) {
final ChannelFactory factory = new NioClientSocketChannelFactory(executor, executor);
final ClientBootstrap bootstrap = new ClientBootstrap(factory);
clientHandler = new SendClientHandler(options);
bootstrap.setPipelineFactory(new ChannelPipelineFactory() {
@Override
public ChannelPipeline getPipeline() throws Exception {
ChannelPipeline pipeline = Channels.pipeline();
pipeline.addLast("handshaker", new ClientHandshakeHandler(options));
pipeline.addLast("decoder", new RtmpDecoder());
pipeline.addLast("encoder", new RtmpEncoderEx());
// 通常のclientHandlerを利用すると、接続だけして、publishする前という動作ができない。
pipeline.addLast("handler", clientHandler);
return pipeline;
}
});
bootstrap.setOption("tcpNoDelay", true);
bootstrap.setOption("keepAlive", true);
return bootstrap;
}
/**
* アドレスの解析処理
* @param options
* @throws Exception
*/
private void parseAddress(ClientOptions options) throws Exception {
Matcher matcher = pattern.matcher(rtmpAddress);
if(!matcher.matches()) {
throw new Exception("rtmp address is invalid");
}
if(matcher.groupCount() != 4) {
throw new Exception("failed to rtmpAddress parse.");
}
options.setHost(matcher.group(1));
if(matcher.group(2) == null) {
options.setPort(1935);
}
else {
options.setPort(Integer.parseInt(matcher.group(2).substring(1)));
}
options.setAppName(matcher.group(3));
}
/**
* 放送開始を実行(接続と放送開始が別になっているのが元なので、ここにあります。)
*/
public void publish() {
// 送信stream名を変更かける場合はここで処理する必要があります。
// stream名が一定なら、別にここでやる必要はないです。
// options.setStreamName();
isPublishing = true; // 放送中フラグをONにします。
clientHandler.publish(); // clientHandlerにpublishの実行をさせます。
}
/**
* 放送を中断
* この処理は内部でclientBootstrapを止めてしまうので、再publishをするには再コネクトが必要になります。
*/
public void unpublish() {
isPublishing = false;
clientHandler.unpublish();
}
/**
* 外から動作を停止させる場合の処理
* こっちは明示的にthreadを終了させてしまいます。
*/
public void stop() {
isWorking = false;
future.getChannel().close();
}
/**
* 停止処理
* 停止したときに呼ばれてやること。
* いまのところ特になし。
*/
@Override
public void close() {
}
/**
* metaデータの応答
*/
@Override
public Metadata getMetadata() {
return metadata;
}
/**
* 配信開始時に一番始めに送信するメッセージ設定
* ここに特殊なコマンドをいれて認証したりしてもいい。
*/
@Override
public RtmpMessage[] getStartMessages() {
return new RtmpMessage[]{metadata};
}
/**
* 再生位置の取得
* 今回つくりたいのはliveデータの転送なので、再生位置のコントロールは基本しません。
*/
@Override
@Deprecated
public long getTimePosition() {
throw new RuntimeException("seek is not supported for live.");
}
/**
* シーク動作
* 禁止します(liveなので)
*/
@Override
public long seek(long timePosition) {
throw new RuntimeException("seek is not supported for live.");
}
/**
* 次のメッセージがくるかどうか
* liveなので、もっていなくてもあとから来ると信じます
* よってtrue固定
*/
@Override
public boolean hasNext() {
return true;
}
/**
* 次のメッセージ要求されたときの動作
* nullを応答するとunpublishするようになっているみたいです。
*/
@Override
public RtmpMessage next() {
if(aggregateDuration <= 0) {
if(!isPublishing || !isWorking) {
return null; // 動作していなければnullを応答
}
try {
FlvAtom atom = dataQueue.take();
logger.info("send flvAtom to server.");
return atom;
}
catch(Exception e) {
logger.error("", e);
return null;
}
}
else {
throw new RuntimeException("chunk for aggregate is not supported.");
}
}
/**
* 集合メッセージ用のデーア設置処理(0がくることを想定しています。)
*/
@Override
public void setAggregateDuration(int targetDuration) {
aggregateDuration = targetDuration;
}
/**
* rtmpに流すflvTagを受け入れます
* @param flvTag
* @throws Exception
*/
public void send(FlvTag flvTag) throws Exception {
// mshは保持しておく。
// publish中じゃなかったら送らない。
// 開始位置がまだ決定していない場合は、timestampを保持しておいて、そこから経過時間とする。
if(flvTag == null) {
return;
}
// 前からのずれは任意で処置しておく
if(flvTag instanceof VideoTag) {
// 映像タグ
VideoTag vTag = (VideoTag)flvTag;
logger.info("videoTag:{}", vTag);
if(vTag.isSequenceHeader()) {
videoMshTag = vTag;
return;
}
else if(vTag.getCodec() != FlvCodecType.H264) {
videoMshTag = null;
}
if(vTag.getCodec() == FlvCodecType.H264 && vTag.isKeyFrame() &&videoMshTag != null) {
// mshも送っておく。
videoMshTag.setPts(vTag.getPts());
dataQueue.add(manager.getAtom(videoMshTag));
}
dataQueue.add(manager.getAtom(flvTag));
}
else if(flvTag instanceof AudioTag) {
// 音声タグ
AudioTag aTag = (AudioTag)flvTag;
logger.info("audioTag:{}", aTag);
if(aTag.isSequenceHeader()) {
audioMshTag = aTag;
return;
}
else if(aTag.getCodec() != FlvCodecType.AAC) {
audioMshTag = null;
}
if(aTag.getCodec() == FlvCodecType.AAC && audioMshTag != null) {
audioMshTag.setPts(aTag.getPts());
dataQueue.add(manager.getAtom(audioMshTag));
}
dataQueue.add(manager.getAtom(flvTag));
}
// 始めの転送では、mshを忘れずにおくるようにしておく。
// データをqueueにためておく。
// 以上でいいはず。
}
}