Fix test for bug #32625
[gnash.git] / utilities / rtmpget.cpp
blob559e52a6f6b061606f1c34e392f5c9e27b3c584e
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>
36 #include <iostream>
38 using namespace gnash;
40 /// The play command is initiated by NetStream.play. This must specify a
41 /// play path, and can add three further optional arguments. These are:
42 /// 1. start: start offset, or -1 for a live stream, or -2 for either
43 /// a live stream if found, or a recorded stream if not.
44 /// 2. length: how long to play, or -1 for all of a live stream, or 0 for
45 /// a single frame.
46 /// 3. reset: (object) no documentation.
47 /// This class should not care about these! NetStream / NetConnection should
48 /// encode all the play arguments and send them. The play packet should be
49 /// on the video channel (what about audio?) and be an "invoke" packet.
51 /// ActionScript can send invoke packets directly using NetConnection call.
53 /// TODO: This class needs a function to be called at heartbeat rate so that
54 /// the data can be processed.
55 /// TODO: Work out which messages should be handled internally and which
56 /// should be made available to the core (maybe all).
57 /// 1. Core events are those that should be available to AS. They
58 /// include: onStatus.
59 /// 2. Internal events are not important to AS. We don't know
60 /// if they ever get through. They certainly aren't documented
61 /// and they may expect a certain response. They include:
62 /// _onbwdone, _onbwcheck, _result. The _result function is
63 /// almost certainly forwarded as onResult.
64 /// 3. The client should send createStream. This is builtin to
65 /// to actionscript.
66 // function NetStream(connection) {
67 //
68 // function OnCreate(nStream) {
69 // this.nStream = nStream;
70 // }
71 //
72 // // Some kind of type thing associating NetStream and NetConnection.
73 // ASnative(2101, 200)(this, connection);
74 //
75 // var _local2 = OnCreate.prototype;
76 //
77 // /// The server should send onResult with stream ID.
78 // //
79 // /// What does the function do? Stores the stream ID somewhere so it
80 // /// can
81 // _local2.onResult = function (streamId) {
82 // ASnative(2101, 201)(this.nStream, streamId);
83 // };
84 //
85 // /// onStatus messages are forwarded to the NetStream object.
86 // _local2.onStatus = function (info) {
87 // this.nStream.onStatus(info);
88 // };
89 //
90 // /// Send invoke packet with createStream. The callback is the OnCreate
91 // /// object.
92 // connection.call("createStream", new OnCreate(this));
93 // }
95 namespace {
96 void usage(std::ostream& o);
99 class FakeNC
101 public:
103 FakeNC()
105 _callCount(0),
106 _seek(0),
107 _len(-1),
108 _stream(0)
111 size_t callNumber() {
112 return _callCount++;
115 void queueCall(size_t n, const std::string& call) {
116 _calls.insert(std::make_pair(n, call));
119 std::string getCall(size_t n) {
120 std::map<size_t, std::string>::iterator i = _calls.find(n);
121 if (i == _calls.end()) return "";
123 std::string s = i->second;
124 _calls.erase(i);
125 return s;
128 void setPlayPath(const std::string& p) {
129 _playpath = p;
132 const std::string& playpath() const {
133 return _playpath;
136 void setSeekTime(double secs) {
137 _seek = secs;
140 double seekTime() const {
141 return _seek;
144 void setLength(double len) {
145 _len = len;
148 double length() const {
149 return _len;
152 void setStreamID(int s) {
153 _stream = s;
156 int streamID() const {
157 return _stream;
160 private:
161 size_t _callCount;
162 std::map<size_t, std::string> _calls;
164 std::string _playpath;
166 double _seek, _len;
168 int _stream;
171 void
172 writeFLVHeader(std::ostream& o)
174 char flvHeader[] = {
175 'F', 'L', 'V', 0x01,
176 0x05,
177 0x00, 0x00, 0x00, 0x09,
178 0x00, 0x00, 0x00, 0x00 // first prevTagSize=0
181 o.write(flvHeader, arraySize(flvHeader));
185 bool handleInvoke(rtmp::RTMP& r, FakeNC& nc, const boost::uint8_t* payload,
186 const boost::uint8_t* end);
188 /// These functions create an RTMP call buffer and send it. They mimic
189 /// NetConnection.call() methods and replies to server calls.
191 /// If a call is initiated by us, we send our own call number.
192 /// If we are replying to a server call, we send the server's call number back.
193 void
194 sendConnectPacket(rtmp::RTMP& r, FakeNC& nc, const std::string& app,
195 const std::string& ver, const std::string& swfurl,
196 const std::string& tcurl, const std::string& pageurl)
198 log_debug("Sending connect packet.");
200 log_debug("app : %s", app);
201 log_debug("flashVer : %s", ver);
202 log_debug("tcURL : %s", tcurl);
203 log_debug("swfURL : %s", swfurl);
204 log_debug("pageURL : %s", pageurl);
206 SimpleBuffer buf;
208 amf::write(buf, "connect");
209 const size_t cn = nc.callNumber();
211 /// Call number?
212 amf::write(buf, static_cast<double>(cn));
214 buf.appendByte(amf::OBJECT_AMF0);
215 if (!app.empty()) amf::writeProperty(buf, "app", app);
216 if (!ver.empty()) amf::writeProperty(buf, "flashVer", ver);
217 if (!swfurl.empty()) amf::writeProperty(buf, "swfUrl", swfurl);
218 if (!tcurl.empty()) amf::writeProperty(buf, "tcUrl", tcurl);
219 amf::writeProperty(buf, "fpad", false);
220 amf::writeProperty(buf, "capabilities", 15.0);
221 amf::writeProperty(buf, "audioCodecs", 3191.0);
222 amf::writeProperty(buf, "videoCodecs", 252.0);
223 amf::writeProperty(buf, "videoFunction", 1.0);
224 if (!pageurl.empty()) amf::writeProperty(buf, "pageUrl", pageurl);
225 buf.appendByte(0);
226 buf.appendByte(0);
227 buf.appendByte(amf::OBJECT_END_AMF0);
229 nc.queueCall(cn, "connect");
230 r.call(buf);
234 void
235 sendCheckBW(rtmp::RTMP& r, FakeNC& nc)
237 SimpleBuffer buf;
239 const size_t cn = nc.callNumber();
241 amf::write(buf, "_checkbw");
242 amf::write(buf, static_cast<double>(cn));
243 buf.appendByte(amf::NULL_AMF0);
245 nc.queueCall(cn, "_checkbw");
246 r.call(buf);
249 void
250 replyBWCheck(rtmp::RTMP& r, FakeNC& /*nc*/, double txn)
252 // Infofield1?
253 SimpleBuffer buf;
254 amf::write(buf, "_result");
255 amf::write(buf, txn);
256 buf.appendByte(amf::NULL_AMF0);
257 amf::write(buf, 0.0);
259 r.call(buf);
263 void
264 sendPausePacket(rtmp::RTMP& r, FakeNC& nc, bool flag, double time)
266 const int streamid = nc.streamID();
268 SimpleBuffer buf;
270 amf::write(buf, "pause");
272 // What is this? The play stream? Call number?
273 amf::write(buf, 0.0);
274 buf.appendByte(amf::NULL_AMF0);
276 log_debug( "Pause: flag=%s, time=%d", flag, time);
277 amf::write(buf, flag);
279 // "this.time", i.e. NetStream.time.
280 amf::write(buf, time * 1000.0);
282 r.play(buf, streamid);
285 // Which channel to send on? Always video?
286 //ASnative(2101, 202)(this, "play", null, name, start * 1000, len * 1000, reset);
287 // This call is not queued (it's a play call, and doesn't have a callback).
288 void
289 sendPlayPacket(rtmp::RTMP& r, FakeNC& nc)
292 const int streamid = nc.streamID();
293 const double seektime = nc.seekTime() * 1000.0;
294 const double length = nc.length() * 1000.0;
296 log_debug("Sending play packet. Stream id: %s, playpath %s", streamid,
297 nc.playpath());
299 SimpleBuffer buf;
301 amf::write(buf, "play");
303 // What is this? The play stream? Call number?
304 amf::write(buf, 0.0);
305 buf.appendByte(amf::NULL_AMF0);
307 log_debug( "seekTime=%.2f, dLength=%d, sending play: %s",
308 seektime, length, nc.playpath());
309 amf::write(buf, nc.playpath());
311 // Optional parameters start and len.
313 // start: -2, -1, 0, positive number
314 // -2: looks for a live stream, then a recorded stream, if not found
315 // any open a live stream
316 // -1: plays a live stream
317 // >=0: plays a recorded streams from 'start' milliseconds
318 amf::write(buf, seektime);
320 // len: -1, 0, positive number
321 // -1: plays live or recorded stream to the end (default)
322 // 0: plays a frame 'start' ms away from the beginning
323 // >0: plays a live or recoded stream for 'len' milliseconds
324 //enc += EncodeNumber(enc, -1.0); // len
325 amf::write(buf, length);
327 r.play(buf, streamid);
330 void
331 sendCreateStream(rtmp::RTMP& r, FakeNC& nc)
333 const size_t cn = nc.callNumber();
335 SimpleBuffer buf;
336 amf::write(buf, "createStream");
337 amf::write(buf, static_cast<double>(cn));
338 buf.appendByte(amf::NULL_AMF0);
339 nc.queueCall(cn, "createStream");
340 r.call(buf);
342 void
343 sendDeleteStream(rtmp::RTMP& r, FakeNC& nc, double id)
345 const size_t cn = nc.callNumber();
347 SimpleBuffer buf;
348 amf::write(buf, "deleteStream");
350 // Call number?
351 amf::write(buf, static_cast<double>(cn));
352 buf.appendByte(amf::NULL_AMF0);
353 amf::write(buf, id);
354 nc.queueCall(cn, "deleteStream");
355 r.call(buf);
358 void
359 sendFCSubscribe(rtmp::RTMP& r, FakeNC& nc, const std::string& subscribepath)
361 const size_t cn = nc.callNumber();
363 SimpleBuffer buf;
364 amf::write(buf, "FCSubscribe");
366 // What is this?
367 amf::write(buf, static_cast<double>(cn));
368 buf.appendByte(amf::NULL_AMF0);
369 amf::write(buf, subscribepath);
371 nc.queueCall(cn, "FCSubscribe");
372 r.call(buf);
375 /// Some URLs to try are:
377 /// -u rtmp://tagesschau.fcod.llnwd.net:1935/a3705/d1
378 /// with -p 2010/0216/TV-20100216-0911-2401.hi or
379 /// -p 2010/0216/TV-20100216-0911-2401.lo
381 /// -u rtmp://ndr.fc.llnwd.net:1935/ndr/_definst_
382 /// with -p ndr_fs_nds_hi_flv *and* -s -1 (live stream)
384 main(int argc, char** argv)
386 const Arg_parser::Option opts[] =
388 { 'h', "help", Arg_parser::no },
389 { 'v', "verbose", Arg_parser::no },
390 { 'u', "url", Arg_parser::yes },
391 { 'p', "playpath", Arg_parser::yes },
392 { 's', "seek", Arg_parser::yes },
393 { 'l', "length", Arg_parser::yes },
394 { 'o', "outfile", Arg_parser::yes }
397 Arg_parser parser(argc, argv, opts);
399 if (!parser.error().empty()) {
400 std::cout << parser.error() << std::endl;
401 std::exit(EXIT_FAILURE);
403 gnash::LogFile& l = gnash::LogFile::getDefaultInstance();
405 std::string url;
406 std::string playpath;
407 std::string tc;
408 std::string swf;
409 std::string page;
410 std::string outf;
412 double seek = 0, len = -1;
414 for (int i = 0; i < parser.arguments(); ++i) {
415 const int code = parser.code(i);
416 try {
417 switch (code) {
418 case 'h':
419 usage(std::cout);
420 exit(EXIT_SUCCESS);
421 case 'u':
422 url = parser.argument(i);
423 break;
424 case 'p':
425 playpath = parser.argument(i);
426 break;
427 case 't':
428 tc = parser.argument(i);
429 break;
430 case 's':
431 seek = parser.argument<double>(i);
432 break;
433 case 'l':
434 len = parser.argument<double>(i);
435 break;
436 case 'o':
437 outf = parser.argument(i);
438 break;
439 case 'v':
440 l.setVerbosity();
441 break;
444 catch (Arg_parser::ArgParserException &e) {
445 std::cerr << _("Error parsing command line: ") << e.what() << "\n";
446 std::exit(EXIT_FAILURE);
450 if (url.empty() || playpath.empty()) {
451 std::cerr << "You must specify URL and playpath\n";
452 std::exit(EXIT_FAILURE);
455 if (outf.empty()) {
456 std::cerr << "No output file specified. Will connect anyway\n";
459 std::ofstream flv;
461 if (!outf.empty()) {
462 flv.open(outf.c_str());
463 if (flv) writeFLVHeader(flv);
466 URL playurl(url);
467 if (tc.empty()) tc = playurl.str();
469 const std::string app = playurl.path().substr(1);
471 std::string ver = "LNX 10,0,22,87";
473 gnash::rtmp::RTMP r;
474 FakeNC nc;
475 nc.setPlayPath(playpath);
476 nc.setLength(len);
477 nc.setSeekTime(seek);
479 log_debug("Initial connection");
481 if (!r.connect(url)) {
482 log_error("Initial connection failed!");
483 std::exit(EXIT_FAILURE);
486 do {
487 r.update();
488 gnashSleep(1000);
489 } while (!r.connected());
491 if (r.error()) {
492 log_error("Connection attempt failed");
493 std::exit(EXIT_FAILURE);
496 /// 1. connect.
497 sendConnectPacket(r, nc, app, ver, swf, tc, page);
499 // Some servers are fine if we send _onbwcheck here, others aren't.
500 // Either way it's a SWF implementation detail, not an automatic
501 // send.
502 //sendCheckBW(r, nc);
504 // Note that rtmpdump sends the "ServerBW" control ping when the connect
505 // call returns.
507 log_debug("Connect packet sent.");
509 while (1) {
510 r.update();
511 if (r.error()) {
512 gnash::log_error("Connection error");
513 break;
516 /// Retrieve messages.
517 boost::shared_ptr<SimpleBuffer> b = r.getMessage();
518 while (b.get()) {
519 handleInvoke(r, nc, b->data() + rtmp::RTMPHeader::headerSize,
520 b->data() + b->size());
521 b = r.getMessage();
524 /// Retrieve video packets.
525 boost::shared_ptr<SimpleBuffer> f = r.getFLVFrame();
526 while (f.get()) {
527 if (flv) {
528 const char* start = reinterpret_cast<const char*>(
529 f->data() + rtmp::RTMPHeader::headerSize);
530 flv.write(start, f->size() - rtmp::RTMPHeader::headerSize);
532 f = r.getMessage();
534 gnashSleep(1000);
540 bool
541 handleInvoke(rtmp::RTMP& r, FakeNC& nc, const boost::uint8_t* payload,
542 const boost::uint8_t* end)
544 assert(payload != end);
546 // make sure it is a string method name we start with
547 if (payload[0] != 0x02) {
548 log_error( "%s, Sanity failed. no string method in invoke packet",
549 __FUNCTION__);
550 return false;
553 ++payload;
554 std::string method = amf::readString(payload, end);
556 log_debug("Invoke: read method string %s", method);
557 if (*payload != amf::NUMBER_AMF0) return false;
558 ++payload;
561 log_debug( "%s, server invoking <%s>", __FUNCTION__, method);
563 bool ret = false;
565 /// _result means it's the answer to a remote method call initiated
566 /// by us.
567 if (method == "_result") {
569 const double txn = amf::readNumber(payload, end);
570 std::string calledMethod = nc.getCall(txn);
572 log_debug("Received result for method call %s (%s)",
573 calledMethod, boost::io::group(std::setprecision(15), txn));
575 if (calledMethod == "connect")
577 // Do here.
578 sendCreateStream(r, nc);
581 else if (calledMethod == "createStream") {
583 log_debug("createStream invoked");
584 if (*payload != amf::NULL_AMF0) return false;
585 ++payload;
587 log_debug("AMF buffer for createStream: %s\n",
588 hexify(payload, end - payload, false));
590 if (*payload != amf::NUMBER_AMF0) return false;
591 ++payload;
592 double sid = amf::readNumber(payload, end);
594 log_debug("Stream ID: %s", sid);
595 nc.setStreamID(sid);
597 /// Issue NetStream.play command.
598 sendPlayPacket(r, nc);
600 /// Allows quick downloading.
601 r.setBufferTime(3600000, nc.streamID());
604 else if (calledMethod == "play") {
605 log_debug("Play called");
607 return ret;
610 /// These are remote function calls initiated by the server .
612 const double txn = amf::readNumber(payload, end);
613 log_debug("Received server call %s %s",
614 boost::io::group(std::setprecision(15), txn),
615 txn ? "" : "(no reply expected)");
617 /// This must return a value. It can be anything.
618 if (method == "onBWCheck") {
619 if (txn) replyBWCheck(r, nc, txn);
620 else {
621 log_error("Expected call number for onBWCheck");
625 /// If the server sends this, we reply (the call should contain a
626 /// callback object!).
627 if (method == "_onbwcheck") {
628 if (txn) replyBWCheck(r, nc, txn);
629 else {
630 log_error("Server called _onbwcheck without a callback");
632 return ret;
635 // This should be called by the server when the bandwidth test is finished.
637 // It contains information, but we don't have to do anything.
638 if (method == "onBWDone") {
639 // This is a SWF implementation detail, not required by the protocol.
640 return ret;
643 if (method == "_onbwdone") {
645 if (*payload != amf::NULL_AMF0) return false;
646 ++payload;
648 log_debug("AMF buffer for _onbwdone: %s\n",
649 hexify(payload, end - payload, false));
651 double latency = amf::readNumber(payload, end);
652 double bandwidth = amf::readNumber(payload, end);
653 log_debug("Latency: %s, bandwidth %s", latency, bandwidth);
654 return ret;
657 /// Don't know when it sends this.
658 if (method == "onFCSubscribe") {
659 return ret;
662 /// Or this.
663 if (method == "onFCUnsubscribe") {
664 r.close();
665 ret = true;
666 return ret;
669 if (method == "_error") {
670 log_error( "rtmp server sent error");
671 std::exit(EXIT_FAILURE);
674 if (method == "close") {
675 log_error( "rtmp server requested close");
676 r.close();
677 return ret;
680 if (method == "onStatus") {
681 if (*payload != amf::NULL_AMF0) return false;
682 ++payload;
683 #if 1
684 log_debug("AMF buffer for onstatus: %s",
685 hexify(payload, end - payload, true));
686 #endif
687 if (*payload != amf::OBJECT_AMF0) {
688 log_debug("not an object");
689 return false;
691 ++payload;
692 if (payload == end) return false;
694 std::string code;
695 std::string level;
696 try {
698 // Hack.
699 while (payload < end && *payload != amf::OBJECT_END_AMF0) {
701 const std::string& n = amf::readString(payload, end);
702 if (n.empty()) continue;
704 //log_debug("read string %s", n);
705 if (payload == end) break;
707 // There's no guarantee that all members are strings, but
708 // it's usually enough for this.
709 if (*payload != amf::STRING_AMF0) {
710 break;
713 ++payload;
714 if (payload == end) break;
716 const std::string& v = amf::readString(payload, end);
717 if (payload == end) break;
718 if (n == "code") code = v;
719 if (n == "level") level = v;
722 catch (const amf::AMFException& e) {
723 throw;
724 return false;
727 if (code.empty() || level.empty()) return false;
729 log_debug("onStatus: %s, %s", code, level);
730 if (code == "NetStream.Failed"
731 || code == "NetStream.Play.Failed"
732 || code == "NetStream.Play.StreamNotFound"
733 || code == "NetConnection.Connect.InvalidApp")
735 r.close();
736 log_error( "Closing connection: %s", code);
737 std::exit(EXIT_SUCCESS);
740 if (code == "NetStream.Play.Start") {
741 log_debug("Netstream.Play.Start called");
742 return ret;
745 // Return 1 if this is a Play.Complete or Play.Stop
746 if (code == "NetStream.Play.Complete" ||
747 code == "NetStream.Play.Stop") {
748 r.close();
749 std::exit(EXIT_SUCCESS);
752 return ret;
755 namespace {
757 void
758 usage(std::ostream& o)
760 o << "usage: rtmpdump -u <app> -p playpath [ -o outfile ]\n";
761 o << "\n";
762 o << "\t-h Show this help and exit\n";
763 o << "\t-u <url> The full url of the rtmp application\n";
764 o << "\t-p <path> The play path of the stream\n";
765 o << "\t-s <sec> Start at the given seek offset\n";
766 o << "\t-l <sec> Retrieve only the specified length in seconds\n";
767 o << "\t-o <file> Output file for video\n";
768 o << "\t-v Verbose output (more 'v's for more verbosity)\n";
769 o << "\n";