Move "loop" related tests in their own dir. This is just to break the ice... ideally...
[gnash.git] / utilities / rtmpget.cpp
blobcc2a49c665cc054ae99a4e052a6c69bfa1882097
1 // rtmpdump.cpp: RTMP file downloader utility
2 //
3 // Copyright (C) 2008, 2009, 2010 Free Software Foundation, Inc.
4 //
5 // This program is free software; you can redistribute it and/or modify
6 // it under the terms of the GNU General Public License as published by
7 // the Free Software Foundation; either version 3 of the License, or
8 // (at your option) any later version.
9 //
10 // This program is distributed in the hope that it will be useful,
11 // but WITHOUT ANY WARRANTY; without even the implied warranty of
12 // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
13 // GNU General Public License for more details.
14 //
15 // You should have received a copy of the GNU General Public License
16 // along with this program; if not, write to the Free Software
17 // Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA
18 //
20 #include "RTMP.h"
21 #include <string>
22 #include "log.h"
23 #include "arg_parser.h"
24 #include "SimpleBuffer.h"
25 #include "AMF.h"
26 #include "GnashAlgorithm.h"
27 #include "GnashSleep.h"
28 #include "URL.h"
30 #include <boost/cstdint.hpp>
31 #include <iomanip>
32 #include <map>
33 #include <algorithm>
34 #include <iterator>
35 #include <fstream>
37 using namespace gnash;
39 /// The play command is initiated by NetStream.play. This must specify a
40 /// play path, and can add three further optional arguments. These are:
41 /// 1. start: start offset, or -1 for a live stream, or -2 for either
42 /// a live stream if found, or a recorded stream if not.
43 /// 2. length: how long to play, or -1 for all of a live stream, or 0 for
44 /// a single frame.
45 /// 3. reset: (object) no documentation.
46 /// This class should not care about these! NetStream / NetConnection should
47 /// encode all the play arguments and send them. The play packet should be
48 /// on the video channel (what about audio?) and be an "invoke" packet.
50 /// ActionScript can send invoke packets directly using NetConnection call.
52 /// TODO: This class needs a function to be called at heartbeat rate so that
53 /// the data can be processed.
54 /// TODO: Work out which messages should be handled internally and which
55 /// should be made available to the core (maybe all).
56 /// 1. Core events are those that should be available to AS. They
57 /// include: onStatus.
58 /// 2. Internal events are not important to AS. We don't know
59 /// if they ever get through. They certainly aren't documented
60 /// and they may expect a certain response. They include:
61 /// _onbwdone, _onbwcheck, _result. The _result function is
62 /// almost certainly forwarded as onResult.
63 /// 3. The client should send createStream. This is builtin to
64 /// to actionscript.
65 // function NetStream(connection) {
66 //
67 // function OnCreate(nStream) {
68 // this.nStream = nStream;
69 // }
70 //
71 // // Some kind of type thing associating NetStream and NetConnection.
72 // ASnative(2101, 200)(this, connection);
73 //
74 // var _local2 = OnCreate.prototype;
75 //
76 // /// The server should send onResult with stream ID.
77 // //
78 // /// What does the function do? Stores the stream ID somewhere so it
79 // /// can
80 // _local2.onResult = function (streamId) {
81 // ASnative(2101, 201)(this.nStream, streamId);
82 // };
83 //
84 // /// onStatus messages are forwarded to the NetStream object.
85 // _local2.onStatus = function (info) {
86 // this.nStream.onStatus(info);
87 // };
88 //
89 // /// Send invoke packet with createStream. The callback is the OnCreate
90 // /// object.
91 // connection.call("createStream", new OnCreate(this));
92 // }
94 namespace {
95 void usage(std::ostream& o);
98 class FakeNC
100 public:
102 FakeNC()
104 _callCount(0),
105 _seek(0),
106 _len(-1),
107 _stream(0)
110 size_t callNumber() {
111 return _callCount++;
114 void queueCall(size_t n, const std::string& call) {
115 _calls.insert(std::make_pair(n, call));
118 std::string getCall(size_t n) {
119 std::map<size_t, std::string>::iterator i = _calls.find(n);
120 if (i == _calls.end()) return "";
122 std::string s = i->second;
123 _calls.erase(i);
124 return s;
127 void setPlayPath(const std::string& p) {
128 _playpath = p;
131 const std::string& playpath() const {
132 return _playpath;
135 void setSeekTime(double secs) {
136 _seek = secs;
139 double seekTime() const {
140 return _seek;
143 void setLength(double len) {
144 _len = len;
147 double length() const {
148 return _len;
151 void setStreamID(int s) {
152 _stream = s;
155 int streamID() const {
156 return _stream;
159 private:
160 size_t _callCount;
161 std::map<size_t, std::string> _calls;
163 std::string _playpath;
165 double _seek, _len;
167 int _stream;
170 void
171 writeFLVHeader(std::ostream& o)
173 char flvHeader[] = {
174 'F', 'L', 'V', 0x01,
175 0x05,
176 0x00, 0x00, 0x00, 0x09,
177 0x00, 0x00, 0x00, 0x00 // first prevTagSize=0
180 o.write(flvHeader, arraySize(flvHeader));
184 bool handleInvoke(rtmp::RTMP& r, FakeNC& nc, const boost::uint8_t* payload,
185 const boost::uint8_t* end);
187 /// These functions create an RTMP call buffer and send it. They mimic
188 /// NetConnection.call() methods and replies to server calls.
190 /// If a call is initiated by us, we send our own call number.
191 /// If we are replying to a server call, we send the server's call number back.
192 void
193 sendConnectPacket(rtmp::RTMP& r, FakeNC& nc, const std::string& app,
194 const std::string& ver, const std::string& swfurl,
195 const std::string& tcurl, const std::string& pageurl)
197 log_debug("Sending connect packet.");
199 log_debug("app : %s", app);
200 log_debug("flashVer : %s", ver);
201 log_debug("tcURL : %s", tcurl);
202 log_debug("swfURL : %s", swfurl);
203 log_debug("pageURL : %s", pageurl);
205 SimpleBuffer buf;
207 amf::write(buf, "connect");
208 const size_t cn = nc.callNumber();
210 /// Call number?
211 amf::write(buf, static_cast<double>(cn));
213 buf.appendByte(amf::OBJECT_AMF0);
214 if (!app.empty()) amf::writeProperty(buf, "app", app);
215 if (!ver.empty()) amf::writeProperty(buf, "flashVer", ver);
216 if (!swfurl.empty()) amf::writeProperty(buf, "swfUrl", swfurl);
217 if (!tcurl.empty()) amf::writeProperty(buf, "tcUrl", tcurl);
218 amf::writeProperty(buf, "fpad", false);
219 amf::writeProperty(buf, "capabilities", 15.0);
220 amf::writeProperty(buf, "audioCodecs", 3191.0);
221 amf::writeProperty(buf, "videoCodecs", 252.0);
222 amf::writeProperty(buf, "videoFunction", 1.0);
223 if (!pageurl.empty()) amf::writeProperty(buf, "pageUrl", pageurl);
224 buf.appendByte(0);
225 buf.appendByte(0);
226 buf.appendByte(amf::OBJECT_END_AMF0);
228 nc.queueCall(cn, "connect");
229 r.call(buf);
233 void
234 sendCheckBW(rtmp::RTMP& r, FakeNC& nc)
236 SimpleBuffer buf;
238 const size_t cn = nc.callNumber();
240 amf::write(buf, "_checkbw");
241 amf::write(buf, static_cast<double>(cn));
242 buf.appendByte(amf::NULL_AMF0);
244 nc.queueCall(cn, "_checkbw");
245 r.call(buf);
248 void
249 replyBWCheck(rtmp::RTMP& r, FakeNC& /*nc*/, double txn)
251 // Infofield1?
252 SimpleBuffer buf;
253 amf::write(buf, "_result");
254 amf::write(buf, txn);
255 buf.appendByte(amf::NULL_AMF0);
256 amf::write(buf, 0.0);
258 r.call(buf);
262 void
263 sendPausePacket(rtmp::RTMP& r, FakeNC& nc, bool flag, double time)
265 const int streamid = nc.streamID();
267 SimpleBuffer buf;
269 amf::write(buf, "pause");
271 // What is this? The play stream? Call number?
272 amf::write(buf, 0.0);
273 buf.appendByte(amf::NULL_AMF0);
275 log_debug( "Pause: flag=%s, time=%d", flag, time);
276 amf::write(buf, flag);
278 // "this.time", i.e. NetStream.time.
279 amf::write(buf, time * 1000.0);
281 r.play(buf, streamid);
284 // Which channel to send on? Always video?
285 //ASnative(2101, 202)(this, "play", null, name, start * 1000, len * 1000, reset);
286 // This call is not queued (it's a play call, and doesn't have a callback).
287 void
288 sendPlayPacket(rtmp::RTMP& r, FakeNC& nc)
291 const int streamid = nc.streamID();
292 const double seektime = nc.seekTime() * 1000.0;
293 const double length = nc.length() * 1000.0;
295 log_debug("Sending play packet. Stream id: %s, playpath %s", streamid,
296 nc.playpath());
298 SimpleBuffer buf;
300 amf::write(buf, "play");
302 // What is this? The play stream? Call number?
303 amf::write(buf, 0.0);
304 buf.appendByte(amf::NULL_AMF0);
306 log_debug( "seekTime=%.2f, dLength=%d, sending play: %s",
307 seektime, length, nc.playpath());
308 amf::write(buf, nc.playpath());
310 // Optional parameters start and len.
312 // start: -2, -1, 0, positive number
313 // -2: looks for a live stream, then a recorded stream, if not found
314 // any open a live stream
315 // -1: plays a live stream
316 // >=0: plays a recorded streams from 'start' milliseconds
317 amf::write(buf, seektime);
319 // len: -1, 0, positive number
320 // -1: plays live or recorded stream to the end (default)
321 // 0: plays a frame 'start' ms away from the beginning
322 // >0: plays a live or recoded stream for 'len' milliseconds
323 //enc += EncodeNumber(enc, -1.0); // len
324 amf::write(buf, length);
326 r.play(buf, streamid);
329 void
330 sendCreateStream(rtmp::RTMP& r, FakeNC& nc)
332 const size_t cn = nc.callNumber();
334 SimpleBuffer buf;
335 amf::write(buf, "createStream");
336 amf::write(buf, static_cast<double>(cn));
337 buf.appendByte(amf::NULL_AMF0);
338 nc.queueCall(cn, "createStream");
339 r.call(buf);
341 void
342 sendDeleteStream(rtmp::RTMP& r, FakeNC& nc, double id)
344 const size_t cn = nc.callNumber();
346 SimpleBuffer buf;
347 amf::write(buf, "deleteStream");
349 // Call number?
350 amf::write(buf, static_cast<double>(cn));
351 buf.appendByte(amf::NULL_AMF0);
352 amf::write(buf, id);
353 nc.queueCall(cn, "deleteStream");
354 r.call(buf);
357 void
358 sendFCSubscribe(rtmp::RTMP& r, FakeNC& nc, const std::string& subscribepath)
360 const size_t cn = nc.callNumber();
362 SimpleBuffer buf;
363 amf::write(buf, "FCSubscribe");
365 // What is this?
366 amf::write(buf, static_cast<double>(cn));
367 buf.appendByte(amf::NULL_AMF0);
368 amf::write(buf, subscribepath);
370 nc.queueCall(cn, "FCSubscribe");
371 r.call(buf);
374 /// Some URLs to try are:
376 /// -u rtmp://tagesschau.fcod.llnwd.net:1935/a3705/d1
377 /// with -p 2010/0216/TV-20100216-0911-2401.hi or
378 /// -p 2010/0216/TV-20100216-0911-2401.lo
380 /// -u rtmp://ndr.fc.llnwd.net:1935/ndr/_definst_
381 /// with -p ndr_fs_nds_hi_flv *and* -s -1 (live stream)
383 main(int argc, char** argv)
385 const Arg_parser::Option opts[] =
387 { 'h', "help", Arg_parser::no },
388 { 'v', "verbose", Arg_parser::no },
389 { 'u', "url", Arg_parser::yes },
390 { 'p', "playpath", Arg_parser::yes },
391 { 's', "seek", Arg_parser::yes },
392 { 'l', "length", Arg_parser::yes },
393 { 'o', "outfile", Arg_parser::yes }
396 Arg_parser parser(argc, argv, opts);
398 if (!parser.error().empty()) {
399 std::cout << parser.error() << std::endl;
400 std::exit(EXIT_FAILURE);
402 gnash::LogFile& l = gnash::LogFile::getDefaultInstance();
404 std::string url;
405 std::string playpath;
406 std::string tc;
407 std::string swf;
408 std::string page;
409 std::string outf;
411 double seek = 0, len = -1;
413 for (int i = 0; i < parser.arguments(); ++i) {
414 const int code = parser.code(i);
415 try {
416 switch (code) {
417 case 'h':
418 usage(std::cout);
419 exit(EXIT_SUCCESS);
420 case 'u':
421 url = parser.argument(i);
422 break;
423 case 'p':
424 playpath = parser.argument(i);
425 break;
426 case 't':
427 tc = parser.argument(i);
428 break;
429 case 's':
430 seek = parser.argument<double>(i);
431 break;
432 case 'l':
433 len = parser.argument<double>(i);
434 break;
435 case 'o':
436 outf = parser.argument(i);
437 break;
438 case 'v':
439 l.setVerbosity();
440 break;
443 catch (Arg_parser::ArgParserException &e) {
444 std::cerr << _("Error parsing command line: ") << e.what() << "\n";
445 std::exit(EXIT_FAILURE);
449 if (url.empty() || playpath.empty()) {
450 std::cerr << "You must specify URL and playpath\n";
451 std::exit(EXIT_FAILURE);
454 if (outf.empty()) {
455 std::cerr << "No output file specified. Will connect anyway\n";
458 std::ofstream flv;
460 if (!outf.empty()) {
461 flv.open(outf.c_str());
462 if (flv) writeFLVHeader(flv);
465 URL playurl(url);
466 if (tc.empty()) tc = playurl.str();
468 const std::string app = playurl.path().substr(1);
470 std::string ver = "LNX 10,0,22,87";
472 gnash::rtmp::RTMP r;
473 FakeNC nc;
474 nc.setPlayPath(playpath);
475 nc.setLength(len);
476 nc.setSeekTime(seek);
478 log_debug("Initial connection");
480 if (!r.connect(url)) {
481 log_error("Initial connection failed!");
482 std::exit(EXIT_FAILURE);
485 do {
486 r.update();
487 gnashSleep(1000);
488 } while (!r.connected());
490 if (r.error()) {
491 log_error("Connection attempt failed");
492 std::exit(EXIT_FAILURE);
495 /// 1. connect.
496 sendConnectPacket(r, nc, app, ver, swf, tc, page);
498 // Some servers are fine if we send _onbwcheck here, others aren't.
499 // Either way it's a SWF implementation detail, not an automatic
500 // send.
501 //sendCheckBW(r, nc);
503 // Note that rtmpdump sends the "ServerBW" control ping when the connect
504 // call returns.
506 log_debug("Connect packet sent.");
508 while (1) {
509 r.update();
510 if (r.error()) {
511 gnash::log_error("Connection error");
512 break;
515 /// Retrieve messages.
516 boost::shared_ptr<SimpleBuffer> b = r.getMessage();
517 while (b.get()) {
518 handleInvoke(r, nc, b->data() + rtmp::RTMPHeader::headerSize,
519 b->data() + b->size());
520 b = r.getMessage();
523 /// Retrieve video packets.
524 boost::shared_ptr<SimpleBuffer> f = r.getFLVFrame();
525 while (f.get()) {
526 if (flv) {
527 const char* start = reinterpret_cast<const char*>(
528 f->data() + rtmp::RTMPHeader::headerSize);
529 flv.write(start, f->size() - rtmp::RTMPHeader::headerSize);
531 f = r.getMessage();
533 gnashSleep(1000);
539 bool
540 handleInvoke(rtmp::RTMP& r, FakeNC& nc, const boost::uint8_t* payload,
541 const boost::uint8_t* end)
543 assert(payload != end);
545 // make sure it is a string method name we start with
546 if (payload[0] != 0x02) {
547 log_error( "%s, Sanity failed. no string method in invoke packet",
548 __FUNCTION__);
549 return false;
552 ++payload;
553 std::string method = amf::readString(payload, end);
555 log_debug("Invoke: read method string %s", method);
556 if (*payload != amf::NUMBER_AMF0) return false;
557 ++payload;
560 log_debug( "%s, server invoking <%s>", __FUNCTION__, method);
562 bool ret = false;
564 /// _result means it's the answer to a remote method call initiated
565 /// by us.
566 if (method == "_result") {
568 const double txn = amf::readNumber(payload, end);
569 std::string calledMethod = nc.getCall(txn);
571 log_debug("Received result for method call %s (%s)",
572 calledMethod, boost::io::group(std::setprecision(15), txn));
574 if (calledMethod == "connect")
576 // Do here.
577 sendCreateStream(r, nc);
580 else if (calledMethod == "createStream") {
582 log_debug("createStream invoked");
583 if (*payload != amf::NULL_AMF0) return false;
584 ++payload;
586 log_debug("AMF buffer for createStream: %s\n",
587 hexify(payload, end - payload, false));
589 if (*payload != amf::NUMBER_AMF0) return false;
590 ++payload;
591 double sid = amf::readNumber(payload, end);
593 log_debug("Stream ID: %s", sid);
594 nc.setStreamID(sid);
596 /// Issue NetStream.play command.
597 sendPlayPacket(r, nc);
599 /// Allows quick downloading.
600 r.setBufferTime(3600000, nc.streamID());
603 else if (calledMethod == "play") {
604 log_debug("Play called");
606 return ret;
609 /// These are remote function calls initiated by the server .
611 const double txn = amf::readNumber(payload, end);
612 log_debug("Received server call %s %s",
613 boost::io::group(std::setprecision(15), txn),
614 txn ? "" : "(no reply expected)");
616 /// This must return a value. It can be anything.
617 if (method == "onBWCheck") {
618 if (txn) replyBWCheck(r, nc, txn);
619 else {
620 log_error("Expected call number for onBWCheck");
624 /// If the server sends this, we reply (the call should contain a
625 /// callback object!).
626 if (method == "_onbwcheck") {
627 if (txn) replyBWCheck(r, nc, txn);
628 else {
629 log_error("Server called _onbwcheck without a callback");
631 return ret;
634 // This should be called by the server when the bandwidth test is finished.
636 // It contains information, but we don't have to do anything.
637 if (method == "onBWDone") {
638 // This is a SWF implementation detail, not required by the protocol.
639 return ret;
642 if (method == "_onbwdone") {
644 if (*payload != amf::NULL_AMF0) return false;
645 ++payload;
647 log_debug("AMF buffer for _onbwdone: %s\n",
648 hexify(payload, end - payload, false));
650 double latency = amf::readNumber(payload, end);
651 double bandwidth = amf::readNumber(payload, end);
652 log_debug("Latency: %s, bandwidth %s", latency, bandwidth);
653 return ret;
656 /// Don't know when it sends this.
657 if (method == "onFCSubscribe") {
658 return ret;
661 /// Or this.
662 if (method == "onFCUnsubscribe") {
663 r.close();
664 ret = true;
665 return ret;
668 if (method == "_error") {
669 log_error( "rtmp server sent error");
670 std::exit(EXIT_FAILURE);
673 if (method == "close") {
674 log_error( "rtmp server requested close");
675 r.close();
676 return ret;
679 if (method == "onStatus") {
680 if (*payload != amf::NULL_AMF0) return false;
681 ++payload;
682 #if 1
683 log_debug("AMF buffer for onstatus: %s",
684 hexify(payload, end - payload, true));
685 #endif
686 if (*payload != amf::OBJECT_AMF0) {
687 log_debug("not an object");
688 return false;
690 ++payload;
691 if (payload == end) return false;
693 std::string code;
694 std::string level;
695 try {
697 // Hack.
698 while (payload < end && *payload != amf::OBJECT_END_AMF0) {
700 const std::string& n = amf::readString(payload, end);
701 if (n.empty()) continue;
703 //log_debug("read string %s", n);
704 if (payload == end) break;
706 // There's no guarantee that all members are strings, but
707 // it's usually enough for this.
708 if (*payload != amf::STRING_AMF0) {
709 break;
712 ++payload;
713 if (payload == end) break;
715 const std::string& v = amf::readString(payload, end);
716 if (payload == end) break;
717 if (n == "code") code = v;
718 if (n == "level") level = v;
721 catch (const amf::AMFException& e) {
722 throw;
723 return false;
726 if (code.empty() || level.empty()) return false;
728 log_debug("onStatus: %s, %s", code, level);
729 if (code == "NetStream.Failed"
730 || code == "NetStream.Play.Failed"
731 || code == "NetStream.Play.StreamNotFound"
732 || code == "NetConnection.Connect.InvalidApp")
734 r.close();
735 log_error( "Closing connection: %s", code);
736 std::exit(EXIT_SUCCESS);
739 if (code == "NetStream.Play.Start") {
740 log_debug("Netstream.Play.Start called");
741 return ret;
744 // Return 1 if this is a Play.Complete or Play.Stop
745 if (code == "NetStream.Play.Complete" ||
746 code == "NetStream.Play.Stop") {
747 r.close();
748 std::exit(EXIT_SUCCESS);
751 return ret;
754 namespace {
756 void
757 usage(std::ostream& o)
759 o << "usage: rtmpdump -u <app> -p playpath [ -o outfile ]\n";
760 o << "\n";
761 o << "\t-h Show this help and exit\n";
762 o << "\t-u <url> The full url of the rtmp application\n";
763 o << "\t-p <path> The play path of the stream\n";
764 o << "\t-s <sec> Start at the given seek offset\n";
765 o << "\t-l <sec> Retrieve only the specified length in seconds\n";
766 o << "\t-o <file> Output file for video\n";
767 o << "\t-v Verbose output (more 'v's for more verbosity)\n";
768 o << "\n";