/*
* 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.client;
import java.io.File;
import java.util.Arrays;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import com.flazr.rtmp.RtmpHandshake;
import com.flazr.rtmp.RtmpReader;
import com.flazr.rtmp.RtmpWriter;
import com.flazr.rtmp.server.ServerStream;
import com.flazr.rtmp.server.ServerStream.PublishType;
import com.flazr.util.Utils;
public class ClientOptions {
private static final Logger logger = LoggerFactory.getLogger(ClientOptions.class);
private ServerStream.PublishType publishType;
private String host = "localhost";
private int port = 1935;
private String appName = "vod";
private String streamName;
private String fileToPublish;
private RtmpReader readerToPublish;
private RtmpWriter writerToSave;
private String saveAs;
private boolean rtmpe;
private Map<String, Object> params;
private Object[] args;
private byte[] clientVersionToUse;
private int start = -2;
private int length = -1;
private int buffer = 100;
private byte[] swfHash;
private int swfSize;
private int load = 1;
private int loop = 1;
private int threads = 10;
private List<ClientOptions> clientOptionsList;
/*
public static void main(String[] args) {
ClientOptions co = new ClientOptions();
co.parseCli(new String[]{
"-version", "00000000", "-live", "-app", "oflaDemo", "-buffer", "0",
"stream1259414892312", "home/apps/vod/IronMan.flv"
});
RtmpClient.connect(co);
}
*/
public ClientOptions() {}
public ClientOptions(String host, String appName, String streamName, String saveAs) {
this(host, 1935, appName, streamName, saveAs, false, null);
}
public ClientOptions(String host, int port, String appName, String streamName, String saveAs,
boolean rtmpe, String swfFile) {
this.host = host;
this.port = port;
this.appName = appName;
this.streamName = streamName;
this.saveAs = saveAs;
this.rtmpe = rtmpe;
if(swfFile != null) {
initSwfVerification(swfFile);
}
}
private static final Pattern URL_PATTERN = Pattern.compile(
"(rtmp.?)://" // 1) protocol
+ "([^/:]+)(:[0-9]+)?/" // 2) host 3) port
+ "([^/]+)/" // 4) app
+ "(.*)" // 5) play
);
public ClientOptions(String url, String saveAs) {
parseUrl(url);
this.saveAs = saveAs;
}
public void parseUrl(String url) {
Matcher matcher = URL_PATTERN.matcher(url);
if (!matcher.matches()) {
throw new RuntimeException("invalid url: " + url);
}
logger.debug("parsing url: {}", url);
String protocol = matcher.group(1);
logger.debug("protocol = '{}'", protocol);
host = matcher.group(2);
logger.debug("host = '{}'", host);
String portString = matcher.group(3);
if (portString == null) {
logger.debug("port is null in url, will use default 1935");
} else {
portString = portString.substring(1); // skip the ':'
logger.debug("port = '{}'", portString);
}
port = portString == null ? 1935 : Integer.parseInt(portString);
appName = matcher.group(4);
logger.debug("app = '{}'", appName);
streamName = matcher.group(5);
logger.debug("playName = '{}'", streamName);
rtmpe = protocol.equalsIgnoreCase("rtmpe");
if(rtmpe) {
logger.debug("rtmpe requested, will use encryption");
}
}
public void publishLive() {
publishType = ServerStream.PublishType.LIVE;
}
public void publishRecord() {
publishType = ServerStream.PublishType.RECORD;
}
public void publishAppend() {
publishType = ServerStream.PublishType.APPEND;
}
//==========================================================================
/*
protected static Options getCliOptions() {
final Options options = new Options();
options.addOption(new Option("help", "print this message"));
options.addOption(OptionBuilder.withArgName("host").hasArg()
.withDescription("host name").create("host"));
options.addOption(OptionBuilder.withArgName("port").hasArg()
.withDescription("port number").create("port"));
options.addOption(OptionBuilder.withArgName("app").hasArg()
.withDescription("app name").create("app"));
options.addOption(OptionBuilder
.withArgName("start").hasArg()
.withDescription("start position (milliseconds)").create("start"));
options.addOption(OptionBuilder.withArgName("length").hasArg()
.withDescription("length (milliseconds)").create("length"));
options.addOption(OptionBuilder.withArgName("buffer").hasArg()
.withDescription("buffer duration (milliseconds)").create("buffer"));
options.addOption(new Option("rtmpe", "use RTMPE (encryption)"));
options.addOption(new Option("live", "publish local file to server in 'live' mode"));
options.addOption(new Option("record", "publish local file to server in 'record' mode"));
options.addOption(new Option("append", "publish local file to server in 'append' mode"));
options.addOption(OptionBuilder.withArgName("property=value").hasArgs(2)
.withValueSeparator().withDescription("add / override connection param").create("D"));
options.addOption(OptionBuilder.withArgName("swf").hasArg()
.withDescription("path to (decompressed) SWF for verification").create("swf"));
options.addOption(OptionBuilder.withArgName("version").hasArg()
.withDescription("client version to use in RTMP handshake (hex)").create("version"));
options.addOption(OptionBuilder.withArgName("load").hasArg()
.withDescription("no. of client connections (load testing)").create("load"));
options.addOption(OptionBuilder.withArgName("loop").hasArg()
.withDescription("for publish mode, loop count").create("loop"));
options.addOption(OptionBuilder.withArgName("threads").hasArg()
.withDescription("for load testing (load) mode, thread pool size").create("threads"));
options.addOption(new Option("file", "spawn connections listed in file (load testing)"));
return options;
}
public boolean parseCli(final String[] args) {
CommandLineParser parser = new GnuParser();
CommandLine line = null;
final Options options = getCliOptions();
try {
line = parser.parse(options, args);
if(line.hasOption("help") || line.getArgs().length == 0) {
HelpFormatter formatter = new HelpFormatter();
formatter.printHelp("client [options] name [saveAs | fileToPublish]"
+ "\n(name can be stream name, URL or load testing script file)", options);
return false;
}
if(line.hasOption("host")) {
host = line.getOptionValue("host");
}
if(line.hasOption("port")) {
port = Integer.valueOf(line.getOptionValue("port"));
}
if(line.hasOption("app")) {
appName = line.getOptionValue("app");
}
if(line.hasOption("start")) {
start = Integer.valueOf(line.getOptionValue("start"));
}
if(line.hasOption("length")) {
length = Integer.valueOf(line.getOptionValue("length"));
}
if(line.hasOption("buffer")) {
buffer = Integer.valueOf(line.getOptionValue("buffer"));
}
if(line.hasOption("rtmpe")) {
rtmpe = true;
}
if(line.hasOption("live")) {
publishLive();
}
if(line.hasOption("record")) {
publishRecord();
}
if(line.hasOption("append")) {
publishAppend();
}
if(line.hasOption("version")) {
clientVersionToUse = Utils.fromHex(line.getOptionValue("version"));
if(clientVersionToUse.length != 4) {
throw new RuntimeException("client version to use has to be 4 bytes long");
}
}
if(line.hasOption("D")) { // TODO integers, TODO extra args for 'play' command
params = new HashMap(line.getOptionProperties("D"));
}
if(line.hasOption("load")) {
load = Integer.valueOf(line.getOptionValue("load"));
if(publishType != null && load > 1) {
throw new RuntimeException("cannot publish in load testing mode");
}
}
if(line.hasOption("threads")) {
threads = Integer.valueOf(line.getOptionValue("threads"));
}
if(line.hasOption("loop")) {
loop = Integer.valueOf(line.getOptionValue("loop"));
if(publishType == null && loop > 1) {
throw new RuntimeException("cannot loop when not in publish mode");
}
}
} catch(Exception e) {
System.err.println("parsing failed: " + e.getMessage());
return false;
}
String[] actualArgs = line.getArgs();
if(line.hasOption("file")) {
String fileName = actualArgs[0];
File file = new File(fileName);
if(!file.exists()) {
throw new RuntimeException("file does not exist: '" + fileName + "'");
}
logger.info("parsing file: {}", file);
try {
FileInputStream fis = new FileInputStream(file);
BufferedReader reader = new BufferedReader(new InputStreamReader(fis));
int i = 0;
String s;
clientOptionsList = new ArrayList<ClientOptions>();
while ((s = reader.readLine()) != null) {
i++;
logger.debug("parsing line {}: {}", i, s);
String[] tempArgs = s.split("\\s");
ClientOptions tempOptions = new ClientOptions();
if(!tempOptions.parseCli(tempArgs)) {
throw new RuntimeException("aborting, parsing failed at line " + i);
}
clientOptionsList.add(tempOptions);
}
reader.close();
fis.close();
} catch(Exception e) {
throw new RuntimeException(e);
}
} else {
Matcher matcher = URL_PATTERN.matcher(actualArgs[0]);
if (matcher.matches()) {
parseUrl(actualArgs[0]);
} else {
streamName = actualArgs[0];
}
}
if(publishType != null) {
if(actualArgs.length < 2) {
System.err.println("fileToPublish is required for publish mode");
return false;
}
fileToPublish = actualArgs[1];
} else if(actualArgs.length > 1) {
saveAs = actualArgs[1];
}
logger.info("options: {}", this);
return true;
}
*/
//==========================================================================
public int getLoad() {
return load;
}
public void setLoad(int load) {
this.load = load;
}
public int getLoop() {
return loop;
}
public void setLoop(int loop) {
this.loop = loop;
}
public String getFileToPublish() {
return fileToPublish;
}
public void setFileToPublish(String fileName) {
this.fileToPublish = fileName;
}
public RtmpReader getReaderToPublish() {
return readerToPublish;
}
public void setReaderToPublish(RtmpReader readerToPublish) {
this.readerToPublish = readerToPublish;
}
public String getAppName() {
return appName;
}
public void setAppName(String appName) {
this.appName = appName;
}
public String getTcUrl() {
return (rtmpe ? "rtmpe://" : "rtmp://") + host + ":" + port + "/" + appName;
}
public void setArgs(Object ... args) {
this.args = args;
}
public Object[] getArgs() {
return args;
}
public void setClientVersionToUse(byte[] clientVersionToUse) {
this.clientVersionToUse = clientVersionToUse;
}
public byte[] getClientVersionToUse() {
return clientVersionToUse;
}
public void initSwfVerification(String pathToLocalSwfFile) {
initSwfVerification(new File(pathToLocalSwfFile));
}
public void initSwfVerification(File localSwfFile) {
logger.info("initializing swf verification data for: " + localSwfFile.getAbsolutePath());
byte[] bytes = Utils.readAsByteArray(localSwfFile);
byte[] hash = Utils.sha256(bytes, RtmpHandshake.CLIENT_CONST);
swfSize = bytes.length;
swfHash = hash;
logger.info("swf verification initialized - size: {}, hash: {}", swfSize, Utils.toHex(swfHash));
}
public void putParam(String key, Object value) {
if(params == null) {
params = new LinkedHashMap<String, Object>();
}
params.put(key, value);
}
public void setParams(Map<String, Object> params) {
this.params = params;
}
public Map<String, Object> getParams() {
return params;
}
public PublishType getPublishType() {
return publishType;
}
public void setPublishType(PublishType publishType) {
this.publishType = publishType;
}
public String getStreamName() {
return streamName;
}
public void setStreamName(String streamName) {
this.streamName = streamName;
}
public int getStart() {
return start;
}
public void setStart(int start) {
this.start = start;
}
public int getLength() {
return length;
}
public void setLength(int length) {
this.length = length;
}
public int getBuffer() {
return buffer;
}
public void setBuffer(int buffer) {
this.buffer = buffer;
}
public String getHost() {
return host;
}
public void setHost(String host) {
this.host = host;
}
public int getPort() {
return port;
}
public void setPort(int port) {
this.port = port;
}
public String getSaveAs() {
return saveAs;
}
public void setSaveAs(String saveAs) {
this.saveAs = saveAs;
}
public boolean isRtmpe() {
return rtmpe;
}
public byte[] getSwfHash() {
return swfHash;
}
public void setSwfHash(byte[] swfHash) {
this.swfHash = swfHash;
}
public int getSwfSize() {
return swfSize;
}
public void setSwfSize(int swfSize) {
this.swfSize = swfSize;
}
public int getThreads() {
return threads;
}
public void setThreads(int threads) {
this.threads = threads;
}
public RtmpWriter getWriterToSave() {
return writerToSave;
}
public void setWriterToSave(RtmpWriter writerToSave) {
this.writerToSave = writerToSave;
}
public List<ClientOptions> getClientOptionsList() {
return clientOptionsList;
}
@Override
public String toString() {
StringBuilder sb = new StringBuilder();
sb.append("[host: '").append(host);
sb.append("' port: ").append(port);
sb.append(" appName: '").append(appName);
sb.append("' streamName: '").append(streamName);
sb.append("' saveAs: '").append(saveAs);
sb.append("' rtmpe: ").append(rtmpe);
sb.append(" publish: ").append(publishType);
if(clientVersionToUse != null) {
sb.append(" clientVersionToUse: '").append(Utils.toHex(clientVersionToUse)).append('\'');
}
sb.append(" start: ").append(start);
sb.append(" length: ").append(length);
sb.append(" buffer: ").append(buffer);
sb.append(" params: ").append(params);
sb.append(" args: ").append(Arrays.toString(args));
if(swfHash != null) {
sb.append(" swfHash: '").append(Utils.toHex(swfHash));
sb.append("' swfSize: ").append(swfSize).append('\'');
}
sb.append(" load: ").append(load);
sb.append(" loop: ").append(loop);
sb.append(" threads: ").append(threads);
sb.append(']');
return sb.toString();
}
}