/* * RED5 Open Source Flash Server - https://github.com/Red5/ * * Copyright 2006-2015 by respective authors (see below). All rights reserved. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package org.red5.client; import java.io.IOException; import java.util.Map; import java.util.Timer; import java.util.TimerTask; import org.red5.client.net.rtmp.ClientExceptionHandler; import org.red5.client.net.rtmp.INetStreamEventHandler; import org.red5.client.net.rtmp.RTMPClient; import org.red5.io.utils.ObjectMap; import org.red5.proxy.StreamingProxy; import org.red5.server.api.event.IEvent; import org.red5.server.api.event.IEventDispatcher; import org.red5.server.api.service.IPendingServiceCall; import org.red5.server.api.service.IPendingServiceCallback; import org.red5.server.net.rtmp.event.IRTMPEvent; import org.red5.server.net.rtmp.event.Notify; import org.red5.server.net.rtmp.status.StatusCodes; import org.red5.server.stream.message.RTMPMessage; /** * Relay a stream from one location to another via RTMP. * * @author Paul Gregoire (mondain@gmail.com) */ public class StreamRelay { // our consumer private static RTMPClient client; // our publisher private static StreamingProxy proxy; // task timer private static Timer timer; // the source being relayed private static String sourceStreamName; /** * Creates a stream client to consume a stream from an end point and a proxy to relay the stream to another end point. * * @param args * application arguments */ public static void main(String... args) { // handle the args if (args == null || args.length < 7) { System.out.println("Not enough args supplied. Usage: <source uri> <source app> <source stream name> <destination uri> <destination app> <destination stream name> <publish mode>"); } else { // parse the args String sourceHost = args[0], destHost = args[3]; String sourceApp = args[1], destApp = args[4]; int sourcePort = 1935, destPort = 1935; sourceStreamName = args[2]; String destStreamName = args[5]; String publishMode = args[6]; //live, record, or append // look to see if port was included in host string int colonIdx = sourceHost.indexOf(':'); if (colonIdx > 0) { sourcePort = Integer.valueOf(sourceHost.substring(colonIdx + 1)); sourceHost = sourceHost.substring(0, colonIdx); System.out.printf("Source host: %s port: %d\n", sourceHost, sourcePort); } colonIdx = destHost.indexOf(':'); if (colonIdx > 0) { destPort = Integer.valueOf(destHost.substring(colonIdx + 1)); destHost = destHost.substring(0, colonIdx); System.out.printf("Destination host: %s port: %d\n", destHost, destPort); } // create a timer timer = new Timer(); // create our publisher proxy = new StreamingProxy(); proxy.setHost(destHost); proxy.setPort(destPort); proxy.setApp(destApp); proxy.init(); proxy.setConnectionClosedHandler(new Runnable() { @Override public void run() { System.out.println("Publish connection has been closed, source will be disconnected"); client.disconnect(); } }); proxy.setExceptionHandler(new ClientExceptionHandler() { @Override public void handleException(Throwable throwable) { throwable.printStackTrace(); System.exit(2); } }); proxy.start(destStreamName, publishMode, new Object[] {}); // wait for the publish state do { try { Thread.sleep(100L); } catch (InterruptedException e) { e.printStackTrace(); } } while (!proxy.isPublished()); System.out.println("Publishing..."); // create the consumer client = new RTMPClient(); client.setStreamEventDispatcher(new StreamEventDispatcher()); client.setStreamEventHandler(new INetStreamEventHandler() { @Override public void onStreamEvent(Notify notify) { System.out.printf("onStreamEvent: %s\n", notify); ObjectMap<?, ?> map = (ObjectMap<?, ?>) notify.getCall().getArguments()[0]; String code = (String) map.get("code"); System.out.printf("<:%s\n", code); if (StatusCodes.NS_PLAY_STREAMNOTFOUND.equals(code)) { System.out.println("Requested stream was not found"); client.disconnect(); } else if (StatusCodes.NS_PLAY_UNPUBLISHNOTIFY.equals(code) || StatusCodes.NS_PLAY_COMPLETE.equals(code)) { System.out.println("Source has stopped publishing or play is complete"); client.disconnect(); } } }); client.setConnectionClosedHandler(new Runnable() { @Override public void run() { System.out.println("Source connection has been closed, proxy will be stopped"); proxy.stop(); } }); client.setExceptionHandler(new ClientExceptionHandler() { @Override public void handleException(Throwable throwable) { throwable.printStackTrace(); System.exit(1); } }); // connect the consumer Map<String, Object> defParams = client.makeDefaultConnectionParams(sourceHost, sourcePort, sourceApp); // add pageurl and swfurl defParams.put("pageUrl", ""); defParams.put("swfUrl", "app:/Red5-StreamRelay.swf"); // indicate for the handshake to generate swf verification data client.setSwfVerification(true); // connect the client client.connect(sourceHost, sourcePort, defParams, new IPendingServiceCallback() { @Override public void resultReceived(IPendingServiceCall call) { System.out.println("connectCallback"); ObjectMap<?, ?> map = (ObjectMap<?, ?>) call.getResult(); String code = (String) map.get("code"); if ("NetConnection.Connect.Rejected".equals(code)) { System.out.printf("Rejected: %s\n", map.get("description")); client.disconnect(); proxy.stop(); } else if ("NetConnection.Connect.Success".equals(code)) { // 1. Wait for onBWDone timer.schedule(new BandwidthStatusTask(), 2000L); } else { System.out.printf("Unhandled response code: %s\n", code); } } }); // keep sleeping main thread while the proxy runs do { try { Thread.sleep(100L); } catch (InterruptedException e) { e.printStackTrace(); } } while (!proxy.isRunning()); // kill the timer //timer.cancel(); System.out.println("Stream relay exit"); } } /** * Dispatches consumer events. */ private static final class StreamEventDispatcher implements IEventDispatcher { @Override public void dispatchEvent(IEvent event) { System.out.println("ClientStream.dispachEvent()" + event.toString()); try { proxy.pushMessage(null, RTMPMessage.build((IRTMPEvent) event)); } catch (IOException e) { e.printStackTrace(); } } } /** * Handles result from subscribe call. */ private static final class SubscribeStreamCallBack implements IPendingServiceCallback { @Override public void resultReceived(IPendingServiceCall call) { System.out.println("resultReceived: " + call); } } /** * Creates a "stream" via playback, this is the source stream. */ private static final class CreateStreamCallback implements IPendingServiceCallback { @Override public void resultReceived(IPendingServiceCall call) { System.out.println("resultReceived: " + call); Double streamId = (Double) call.getResult(); System.out.println("stream id: " + streamId); // send our buffer size request if (sourceStreamName.endsWith(".flv") || sourceStreamName.endsWith(".f4v") || sourceStreamName.endsWith(".mp4")) { client.play(streamId, sourceStreamName, 0, -1); } else { client.play(streamId, sourceStreamName, -1, 0); } } } /** * Continues to check for onBWDone */ private static final class BandwidthStatusTask extends TimerTask { @Override public void run() { // check for onBWDone System.out.println("Bandwidth check done: " + client.isBandwidthCheckDone()); // cancel this task this.cancel(); // create a task to wait for subscribed timer.schedule(new PlayStatusTask(), 1000L); // 2. send FCSubscribe client.subscribe(new SubscribeStreamCallBack(), new Object[] { sourceStreamName }); } } private static final class PlayStatusTask extends TimerTask { @Override public void run() { // checking subscribed System.out.println("Subscribed: " + client.isSubscribed()); // cancel this task this.cancel(); // 3. create stream client.createStream(new CreateStreamCallback()); } } }