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>
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
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
65 // function NetStream(connection) {
67 // function OnCreate(nStream) {
68 // this.nStream = nStream;
71 // // Some kind of type thing associating NetStream and NetConnection.
72 // ASnative(2101, 200)(this, connection);
74 // var _local2 = OnCreate.prototype;
76 // /// The server should send onResult with stream ID.
78 // /// What does the function do? Stores the stream ID somewhere so it
80 // _local2.onResult = function (streamId) {
81 // ASnative(2101, 201)(this.nStream, streamId);
84 // /// onStatus messages are forwarded to the NetStream object.
85 // _local2.onStatus = function (info) {
86 // this.nStream.onStatus(info);
89 // /// Send invoke packet with createStream. The callback is the OnCreate
91 // connection.call("createStream", new OnCreate(this));
95 void usage(std::ostream
& o
);
110 size_t callNumber() {
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
;
127 void setPlayPath(const std::string
& p
) {
131 const std::string
& playpath() const {
135 void setSeekTime(double secs
) {
139 double seekTime() const {
143 void setLength(double len
) {
147 double length() const {
151 void setStreamID(int s
) {
155 int streamID() const {
161 std::map
<size_t, std::string
> _calls
;
163 std::string _playpath
;
171 writeFLVHeader(std::ostream
& o
)
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.
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
);
207 amf::write(buf
, "connect");
208 const size_t cn
= nc
.callNumber();
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
);
226 buf
.appendByte(amf::OBJECT_END_AMF0
);
228 nc
.queueCall(cn
, "connect");
234 sendCheckBW(rtmp::RTMP
& r
, FakeNC
& nc
)
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");
249 replyBWCheck(rtmp::RTMP
& r
, FakeNC
& /*nc*/, double txn
)
253 amf::write(buf
, "_result");
254 amf::write(buf
, txn
);
255 buf
.appendByte(amf::NULL_AMF0
);
256 amf::write(buf
, 0.0);
263 sendPausePacket(rtmp::RTMP
& r
, FakeNC
& nc
, bool flag
, double time
)
265 const int streamid
= nc
.streamID();
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).
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
,
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
);
330 sendCreateStream(rtmp::RTMP
& r
, FakeNC
& nc
)
332 const size_t cn
= nc
.callNumber();
335 amf::write(buf
, "createStream");
336 amf::write(buf
, static_cast<double>(cn
));
337 buf
.appendByte(amf::NULL_AMF0
);
338 nc
.queueCall(cn
, "createStream");
342 sendDeleteStream(rtmp::RTMP
& r
, FakeNC
& nc
, double id
)
344 const size_t cn
= nc
.callNumber();
347 amf::write(buf
, "deleteStream");
350 amf::write(buf
, static_cast<double>(cn
));
351 buf
.appendByte(amf::NULL_AMF0
);
353 nc
.queueCall(cn
, "deleteStream");
358 sendFCSubscribe(rtmp::RTMP
& r
, FakeNC
& nc
, const std::string
& subscribepath
)
360 const size_t cn
= nc
.callNumber();
363 amf::write(buf
, "FCSubscribe");
366 amf::write(buf
, static_cast<double>(cn
));
367 buf
.appendByte(amf::NULL_AMF0
);
368 amf::write(buf
, subscribepath
);
370 nc
.queueCall(cn
, "FCSubscribe");
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();
405 std::string playpath
;
411 double seek
= 0, len
= -1;
413 for (int i
= 0; i
< parser
.arguments(); ++i
) {
414 const int code
= parser
.code(i
);
421 url
= parser
.argument(i
);
424 playpath
= parser
.argument(i
);
427 tc
= parser
.argument(i
);
430 seek
= parser
.argument
<double>(i
);
433 len
= parser
.argument
<double>(i
);
436 outf
= parser
.argument(i
);
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
);
455 std::cerr
<< "No output file specified. Will connect anyway\n";
461 flv
.open(outf
.c_str());
462 if (flv
) writeFLVHeader(flv
);
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";
474 nc
.setPlayPath(playpath
);
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
);
488 } while (!r
.connected());
491 log_error("Connection attempt failed");
492 std::exit(EXIT_FAILURE
);
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
501 //sendCheckBW(r, nc);
503 // Note that rtmpdump sends the "ServerBW" control ping when the connect
506 log_debug("Connect packet sent.");
511 gnash::log_error("Connection error");
515 /// Retrieve messages.
516 boost::shared_ptr
<SimpleBuffer
> b
= r
.getMessage();
518 handleInvoke(r
, nc
, b
->data() + rtmp::RTMPHeader::headerSize
,
519 b
->data() + b
->size());
523 /// Retrieve video packets.
524 boost::shared_ptr
<SimpleBuffer
> f
= r
.getFLVFrame();
527 const char* start
= reinterpret_cast<const char*>(
528 f
->data() + rtmp::RTMPHeader::headerSize
);
529 flv
.write(start
, f
->size() - rtmp::RTMPHeader::headerSize
);
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",
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;
560 log_debug( "%s, server invoking <%s>", __FUNCTION__
, method
);
564 /// _result means it's the answer to a remote method call initiated
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")
577 sendCreateStream(r
, nc
);
580 else if (calledMethod
== "createStream") {
582 log_debug("createStream invoked");
583 if (*payload
!= amf::NULL_AMF0
) return false;
586 log_debug("AMF buffer for createStream: %s\n",
587 hexify(payload
, end
- payload
, false));
589 if (*payload
!= amf::NUMBER_AMF0
) return false;
591 double sid
= amf::readNumber(payload
, end
);
593 log_debug("Stream ID: %s", 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");
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
);
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
);
629 log_error("Server called _onbwcheck without a callback");
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.
642 if (method
== "_onbwdone") {
644 if (*payload
!= amf::NULL_AMF0
) return false;
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
);
656 /// Don't know when it sends this.
657 if (method
== "onFCSubscribe") {
662 if (method
== "onFCUnsubscribe") {
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");
679 if (method
== "onStatus") {
680 if (*payload
!= amf::NULL_AMF0
) return false;
683 log_debug("AMF buffer for onstatus: %s",
684 hexify(payload
, end
- payload
, true));
686 if (*payload
!= amf::OBJECT_AMF0
) {
687 log_debug("not an object");
691 if (payload
== end
) return false;
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
) {
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
) {
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")
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");
744 // Return 1 if this is a Play.Complete or Play.Stop
745 if (code
== "NetStream.Play.Complete" ||
746 code
== "NetStream.Play.Stop") {
748 std::exit(EXIT_SUCCESS
);
757 usage(std::ostream
& o
)
759 o
<< "usage: rtmpdump -u <app> -p playpath [ -o outfile ]\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";