1 // rtmpdump.cpp: RTMP file downloader utility
3 // Copyright (C) 2008, 2009, 2010 Free Software Foundation, Inc.
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.
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.
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
23 #include "arg_parser.h"
24 #include "SimpleBuffer.h"
26 #include "GnashAlgorithm.h"
27 #include "GnashSleep.h"
30 #include <boost/cstdint.hpp>
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
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
66 // function NetStream(connection) {
68 // function OnCreate(nStream) {
69 // this.nStream = nStream;
72 // // Some kind of type thing associating NetStream and NetConnection.
73 // ASnative(2101, 200)(this, connection);
75 // var _local2 = OnCreate.prototype;
77 // /// The server should send onResult with stream ID.
79 // /// What does the function do? Stores the stream ID somewhere so it
81 // _local2.onResult = function (streamId) {
82 // ASnative(2101, 201)(this.nStream, streamId);
85 // /// onStatus messages are forwarded to the NetStream object.
86 // _local2.onStatus = function (info) {
87 // this.nStream.onStatus(info);
90 // /// Send invoke packet with createStream. The callback is the OnCreate
92 // connection.call("createStream", new OnCreate(this));
96 void usage(std::ostream
& o
);
111 size_t callNumber() {
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
;
128 void setPlayPath(const std::string
& p
) {
132 const std::string
& playpath() const {
136 void setSeekTime(double secs
) {
140 double seekTime() const {
144 void setLength(double len
) {
148 double length() const {
152 void setStreamID(int s
) {
156 int streamID() const {
162 std::map
<size_t, std::string
> _calls
;
164 std::string _playpath
;
172 writeFLVHeader(std::ostream
& o
)
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.
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
);
208 amf::write(buf
, "connect");
209 const size_t cn
= nc
.callNumber();
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
);
227 buf
.appendByte(amf::OBJECT_END_AMF0
);
229 nc
.queueCall(cn
, "connect");
235 sendCheckBW(rtmp::RTMP
& r
, FakeNC
& nc
)
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");
250 replyBWCheck(rtmp::RTMP
& r
, FakeNC
& /*nc*/, double txn
)
254 amf::write(buf
, "_result");
255 amf::write(buf
, txn
);
256 buf
.appendByte(amf::NULL_AMF0
);
257 amf::write(buf
, 0.0);
264 sendPausePacket(rtmp::RTMP
& r
, FakeNC
& nc
, bool flag
, double time
)
266 const int streamid
= nc
.streamID();
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).
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
,
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
);
331 sendCreateStream(rtmp::RTMP
& r
, FakeNC
& nc
)
333 const size_t cn
= nc
.callNumber();
336 amf::write(buf
, "createStream");
337 amf::write(buf
, static_cast<double>(cn
));
338 buf
.appendByte(amf::NULL_AMF0
);
339 nc
.queueCall(cn
, "createStream");
343 sendDeleteStream(rtmp::RTMP
& r
, FakeNC
& nc
, double id
)
345 const size_t cn
= nc
.callNumber();
348 amf::write(buf
, "deleteStream");
351 amf::write(buf
, static_cast<double>(cn
));
352 buf
.appendByte(amf::NULL_AMF0
);
354 nc
.queueCall(cn
, "deleteStream");
359 sendFCSubscribe(rtmp::RTMP
& r
, FakeNC
& nc
, const std::string
& subscribepath
)
361 const size_t cn
= nc
.callNumber();
364 amf::write(buf
, "FCSubscribe");
367 amf::write(buf
, static_cast<double>(cn
));
368 buf
.appendByte(amf::NULL_AMF0
);
369 amf::write(buf
, subscribepath
);
371 nc
.queueCall(cn
, "FCSubscribe");
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();
406 std::string playpath
;
412 double seek
= 0, len
= -1;
414 for (int i
= 0; i
< parser
.arguments(); ++i
) {
415 const int code
= parser
.code(i
);
422 url
= parser
.argument(i
);
425 playpath
= parser
.argument(i
);
428 tc
= parser
.argument(i
);
431 seek
= parser
.argument
<double>(i
);
434 len
= parser
.argument
<double>(i
);
437 outf
= parser
.argument(i
);
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
);
456 std::cerr
<< "No output file specified. Will connect anyway\n";
462 flv
.open(outf
.c_str());
463 if (flv
) writeFLVHeader(flv
);
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";
475 nc
.setPlayPath(playpath
);
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
);
489 } while (!r
.connected());
492 log_error("Connection attempt failed");
493 std::exit(EXIT_FAILURE
);
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
502 //sendCheckBW(r, nc);
504 // Note that rtmpdump sends the "ServerBW" control ping when the connect
507 log_debug("Connect packet sent.");
512 gnash::log_error("Connection error");
516 /// Retrieve messages.
517 boost::shared_ptr
<SimpleBuffer
> b
= r
.getMessage();
519 handleInvoke(r
, nc
, b
->data() + rtmp::RTMPHeader::headerSize
,
520 b
->data() + b
->size());
524 /// Retrieve video packets.
525 boost::shared_ptr
<SimpleBuffer
> f
= r
.getFLVFrame();
528 const char* start
= reinterpret_cast<const char*>(
529 f
->data() + rtmp::RTMPHeader::headerSize
);
530 flv
.write(start
, f
->size() - rtmp::RTMPHeader::headerSize
);
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",
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;
561 log_debug( "%s, server invoking <%s>", __FUNCTION__
, method
);
565 /// _result means it's the answer to a remote method call initiated
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")
578 sendCreateStream(r
, nc
);
581 else if (calledMethod
== "createStream") {
583 log_debug("createStream invoked");
584 if (*payload
!= amf::NULL_AMF0
) return false;
587 log_debug("AMF buffer for createStream: %s\n",
588 hexify(payload
, end
- payload
, false));
590 if (*payload
!= amf::NUMBER_AMF0
) return false;
592 double sid
= amf::readNumber(payload
, end
);
594 log_debug("Stream ID: %s", 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");
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
);
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
);
630 log_error("Server called _onbwcheck without a callback");
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.
643 if (method
== "_onbwdone") {
645 if (*payload
!= amf::NULL_AMF0
) return false;
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
);
657 /// Don't know when it sends this.
658 if (method
== "onFCSubscribe") {
663 if (method
== "onFCUnsubscribe") {
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");
680 if (method
== "onStatus") {
681 if (*payload
!= amf::NULL_AMF0
) return false;
684 log_debug("AMF buffer for onstatus: %s",
685 hexify(payload
, end
- payload
, true));
687 if (*payload
!= amf::OBJECT_AMF0
) {
688 log_debug("not an object");
692 if (payload
== end
) return false;
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
) {
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
) {
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")
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");
745 // Return 1 if this is a Play.Complete or Play.Stop
746 if (code
== "NetStream.Play.Complete" ||
747 code
== "NetStream.Play.Stop") {
749 std::exit(EXIT_SUCCESS
);
758 usage(std::ostream
& o
)
760 o
<< "usage: rtmpdump -u <app> -p playpath [ -o outfile ]\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";