1. Requirements
- Some spare time and basic perl programming skills
- Mplayer/ffplay and mpg123
- Wireshark
- Perl 5 with following modules:
- Term::ANSIColor (optional)
- WWW::Curl::Easy
- Getopt::Long
- Time::HiRes
- IO::Lambda
- IO::Socket
- HTTP::Request
- HTTP::Response
- URI::Escape
- Mojo::DOM
2. What does the script do?
The script processes FLV stream by deleting audio/video tags. It is also possible to obtain a raw content of the tags (eg. mp3). Data is processed on fly, so there is no necessity to download whole file to listen favorite music from eg. YouTube.
3. Legal notice
The script uses direct YouTube FLV stream which is inconsistent with YouTube terms. This feature was included only for education purposes. To avoid undesired usage I didn’t publish the CGI version of the script.
4. Usage
1. perl extract.pl -u http://www.youtube.com/watch?v=s0NlNOI5LG0 -f 5 -g AV 2> /dev/null | mplayer - # Returns whole FLV stream (audio: MP3, video: Sorensen H.263) 2. perl extract.pl -u http://www.youtube.com/watch?v=8UVNT4wvIGY -f 5 -g Audio 2> /dev/null | mplayer - # Returns "audio only" FLV stream (audio: MP3) 3. perl extract.pl -u http://www.youtube.com/watch?v=8UVNT4wvIGY -f 5 -g Audio -r 2> /dev/null | mpg123 - # Returns raw mp3 stream 4. perl extract.pl -u http://mkaczanowski.com/demos/test.flv -g Video 2> /dev/null | mplayer - # Returns "video only" FLV stream (video: Sorensen H.263) 5. perl extract.pl -u http://www.youtube.com/watch?v=8UVNT4wvIGY -g Audio -t 01m20s 2> /dev/null | mplayer - # Returns "audio only" FLV stream starting from 01:20 (audio: AAC) 6. perl extract.pl -u http://mkaczanowski.com/demos/test.flv -g Audio 1> /dev/null # See debug messages. "1> /dev/null" redirects sdtout to /dev/null, so only stderr is visible 7. perl extract.pl -u http://www.youtube.com/watch?v=8UVNT4wvIGY -f 34 -g Audio -t 01m20s 1> /dev/null # See debug messages. "1> /dev/null" redirects sdtout to /dev/null, so only stderr is visible 8. perl extract.pl -u http://mkaczanowski.com/demos/test.flv -g Audio -r 2> /dev/null | mpg123 - # Returns raw mp3 stream (local url)
4. FLV format specification
Before we start programming let’s cover some theory. After reading specification (links below) you will have to know answers to the following questions:
1. What is the FLV header?
2. What is „tag” and what are the its types?
3. How are the tags placed? What is the order?
4. What are the flags?
5. What are the specific data types (such as uint8, uint32_be etc)?
6. What is the big endian notation?
Links:
1. Specification made by Osflash.org
2. Official specification by Adobe
5. Get YouTube video URI using wireshark
6. Extract.pl – main file
Extract.pl is responsible for handling command line arguments and for creating an appropriate object ( depending on given URL ).
#!/usr/bin/perl use warnings; use strict; use Getopt::Long; use YouTube; use Print; use Local; use Switch; # Local Variables my $params = {}; my $obj; # holds 'Youtube' or 'Local' object # Collect parameters GetOptions ('help|?' => sub{ showHelp(); }, 'url=s' => \$params->{_url}, 'time:s'=>sub{ $params->{_startFromTime} = timeToMsecs($_[1]); }, 'fmt:i'=>\$params->{_selectedFmt}, 'raw' => \$params->{_raw}, 'get=s' => \$params->{_get} # Audio, Video, AV ); # Mandatory params if(!defined($params->{_url}) || !defined($params->{_get})){ showHelp(); } # Check if url contains youtube.com address if($params->{_url} =~ m/(youtube.com)/){ $obj = new YouTube(); $obj->{_url} = $params->{_url}; $obj->{_startFromTime} = $params->{_startFromTime}; # Time in milliseconds $obj->{_selectedFmt} = $params->{_selectedFmt}; # YT fmt -> http://en.wikipedia.org/wiki/YouTube }else{ $obj = new Local(); $obj->{_url} = $params->{_url}; } switch($params->{_get}){ case "Audio" { if(!defined($params->{_raw})){ $obj->getAudio(); }else{ $obj->getRawAudio(); } } case "Video" { if(!defined($params->{_raw})){ $obj->getVideo(); }else{ $obj->getRawVideo(); } } case "AV" { $obj->getAV(); } default { Print::err("Get: Audio, Video, AV options are avalible"); } } sub showHelp{ print "Options: \n"; print "\t --url http://www.youtube.com/watch?v=VIDEO_ID\t #YouTube video url address\n"; print "\t --time ##m##s\t\t\t\t\t #Start playing from given time\n"; print "\t --fmt FMT_VAL\t\t\t\t\t #Choose fmt manually\n"; print "\t --raw\t\t\t\t\t\t #Raw output\n"; print "\t --help\t\t\t\t\t\t #Show this menu\n"; exit; } sub timeToMsecs{ my $str = shift; if(!($str =~ m/([0-9]+)m([0-5][0-9])s/)){ return 0; } $str =~ s//([0-9]+)m([0-5][0-9])s/; return($1*60+$2)*1000; }
7. YouTube.pm
YouTube.pm downloads site, parse it and finds the video URI which is passed to Download.pm.
#!/usr/bin/perl package YouTube; use WWW::Curl::Easy; use URI::Escape; use Mojo::DOM; use Download; use FLV; sub new{ my $class = shift; my $self = { _url => undef, # contains stream url _startFromTime => shift || 0, _fmt => {}, # Fmt list _html => [], # HTML code of YouTube site _cookies => [], # Part of HTTP Header _selectedFmt => undef, _httpReferer => undef, # Part of HTTP Header _download => undef # contains 'download' object }; bless $self, $class; return $self; } sub init { my $self = shift; if(!defined($self->{_url})){ Print::err("URL is missing"); } $self->getHtml(); $self->parseHtml(); $self->{_url} = $self->{_fmt}{$self->{_selectedFmt}}{url}; } # Prints FLV stream whith only audio tags sub getAudio{ my $self = shift; $self->run(0,1,0); } # Prints codec part from audio tags sub getRawAudio{ my $self = shift; $self->run(0,1,1); } # Prints FLV stream with only video tags sub getVideo{ my $self = shift; $self->run(1,0,0); } # Prints codec part from video tags sub getRawVideo{ my $self = shift; $self->run(1,0,1); } # Prints both: audio and video as FLV stream sub getAV{ my $self = shift; $self->run(1,1,0); } # Start downloading sub run{ my $self = shift; $self->init(); $self->{_download} = new Download($self); $self->{_download}->start(); $self->summary(); my $flv = new FLV(shift, shift, shift); $flv->getTags(); } # Download YouTube website sub getHtml{ my $self = shift; my $curl = WWW::Curl::Easy->new; my $response; my $headers; $curl->setopt(CURLOPT_HEADER,0); $curl->setopt(CURLOPT_COOKIESESSION, 1); $curl->setopt(CURLOPT_USERAGENT, Download::getUserAgent()); $curl->setopt(CURLOPT_URL, $self->{_url}); $curl->setopt(CURLOPT_WRITEDATA,\$response); $curl->setopt(CURLOPT_WRITEHEADER, \$headers); $curl->perform(); if ($curl->getinfo(CURLINFO_HTTP_CODE) == 200) { my @headers = split /\n/, $headers; @{$self->{_html}} = split /\n/, $response; @{$self->{_cookies}} = grep {/Set\-Cookie/} @headers; # extract cookies # Parse cookies for(my $i = 0; $i {_cookies}}; $i++){ $self->{_cookies}[$i] =~ s/(\\n|\s+)//g; my @temp = split(/\;/, $self->{_cookies}[$i], 2); $self->{_cookies}[$i] = substr($temp[0],11); } } else { Print::err("Download Error"); } } sub parseHtml{ my $self = shift; my @explode = grep {/flashvars/} @{$self->{_html}}; # extract embed tag which is contained inside of flashvars parameter if($#explode == -1){ Print::err("flashvars NOT FOUND"); } $explode[0] =~ s/\\u([[:xdigit:]]{1,4})/chr(eval("0x$1"))/egis; # convert unicode to asci characters $explode[0] =~ s/(\;|\n|\\")//g; # remove ";", "\n", " ", \" (|\s+|) from string $explode[0] =~ s/\\\//\//g; # change \/ to / my $dom = Mojo::DOM->new($explode[0]); $self->{_httpReferer} = $dom->embed->{src}; my %embed_params; foreach(split(/\&/,$dom->embed->{flashvars})){ # get List of embed params (my $param, my $val) = split(/\=/, $_); $embed_params{$param} = $val; } foreach(split(/\,/,uri_unescape($embed_params{fmt_list}))){ # get List of avalible FMT (my $id, my $resolution) = split(/\//, $_, 3); $self->{_fmt}{$id} = {'resolution' => $resolution}; } foreach(split(/\,/,uri_unescape($embed_params{url_encoded_fmt_stream_map}))){ # get Url List my @spli = split(/itag\=/,$_); $self->{_fmt}{$spli[1]}{'url'} = substr uri_unescape($_), 4; } # Check if _selectedFmt is defined or sort and select lowest fmt if(!defined($self->{'_selectedFmt'}) || (defined($self->{'_selectedFmt'}) && !defined($self->{'_fmt'}{$self->{'_selectedFmt'}}))){ $self->{'_selectedFmt'} = (sort {$a $b} keys %{$self->{'_fmt'}})[0]; } } # Prepare HTTP header sub getHeader{ my $self = shift; my $url = shift || exit; my $return = ""; $url =~ m|(\w+)://([^/:]+)(:\d+)?/(.*)|; $return .= "Host: $2\r\n"; $return .= "Connection: keep-alive\r\n"; $return .= "Referer: ".$self->{_httpReferer}."\r\n"; $return .= "Accept: */*\r\n"; $return .= "Accept-Encoding: gzip,deflate,sdch\r\n"; $return .= "Accept-Language: en-US,pl;q=0.8,en-US;q=0.6,en;q=0.4\r\n"; $return .= "Accept-Charset: Accept-Charset: UTF-8,utf-8;q=0.7,*;q=0.3\r\n"; $return .= "Cookie: ".$self->{_cookies}[0].";".$self->{_cookies}[1].";".$self->{_cookies}[2].";".$self->{_cookies}[3]."&fv=10.1.103\r\n"; return $return; } sub summary{ my $self = shift; Print::info("FMT: ".$self->{_selectedFmt}); Print::info("Time: ".$self->{_startFromTime}."ms"); Print::info("Resolution: ".$self->{_fmt}{$self->{_selectedFmt}}{resolution}); Print::info("Http Referer: ".$self->{_httpReferer}); Print::info("URI: ".$self->{_download}->{_uri}); Print::info("Host: ".$self->{_download}->{_host}); } 1;
8. Local.pm
Local.pm handles “direct” URL (different from YouTube address) and pass it to Download.pm.
#!/usr/bin/perl package Local; use Download; use FLV; sub new{ my $class = shift; my $self = { _url => undef, _download => undef # Holds 'download' object }; bless $self, $class; return $self; } # Prints FLV stream whith only audio tags sub getAudio{ my $self = shift; $self->run(0,1,0); } # Prints codec part from audio tags sub getRawAudio{ my $self = shift; $self->run(0,1,1); } # Prints FLV stream with only video tags sub getVideo{ my $self = shift; $self->run(1,0,0); } # Prints codec part from video tags sub getRawVideo{ my $self = shift; $self->run(1,0,1); } # Prints both: audio and video as FLV stream sub getAV{ my $self = shift; $self->run(1,1,0); } # Starts downloading sub run{ my $self = shift; $self->{_download} = new Download($self); $self->{_download}->start(); $self->summary(); # Print summary of connection such as Host, URI my $flv = new FLV(shift, shift, shift); $flv->getTags(); # Print out tags } # Prepares HTTP header sub getHeader{ my $self = shift; my $url = shift || exit; my $return = ""; $url =~ m|(\w+)://([^/:]+)(:\d+)?/(.*)|; # split url $return .= "Host: $2\r\n"; $return .= "Connection: keep-alive\r\n"; $return .= "Accept: */*\r\n"; return $return; } # Prints URI and HOST to stderr sub summary{ my $self = shift; Print::info("URI: ".$self->{_download}->{_uri}); Print::info("Host: ".$self->{_download}->{_host}); } 1;
9. Download.pm
In general Download.pm handles the Sock.pm operations (such as write, read). Also perpares headers and requests.
#!/usr/bin/perl package Download; use IO::Lambda qw(:all); use IO::Socket; use HTTP::Request; use HTTP::Response; use URI::Escape; use Sock; sub new{ my $class = shift; my $self = { _obj => shift || die("YT object is missing"), _contentLength => undef, _uri => undef, _host => undef }; bless $self, $class; return $self; } # Prepares request and starts downloading! sub start{ my $self = shift; my $request = HTTP::Request-> new( GET => $self->{_obj}->{_url}); my $q = $self->talkRedirect($request); $q->wait; } sub talkRedirect{ my $self = shift; my $req = shift; lambda{ context $self->talk($req); tail{ my @tempHeader; my $newUrl; while (defined($_ = Sock->instance()->get()->getline)){ # read line from socket last if($_ =~ /^[\r\n]+$/); if($_ =~ /Location/){ (my $first, $newUrl) = split /\ /,$_; } if($_ =~ /Content\-Length/){ $_ =~ s/.Length\: /\r/; $self->{_contentLength} = $_;} # save ContentLength $tempHeader[$#tempHeader+1] = $_; } my $res = HTTP::Response->parse(@tempHeader); return $res unless $res->code == 302; # if response code == 302 prepare and send another request $req->uri($newUrl); context $self->talk($req); again; } } } sub talk{ my $self = shift; my $req = shift; my $url = $req->uri; my($uri, $host) = $self->getUriHost($url); my @spl = split(/\&quality/, $uri, 0); $uri = $spl[0]; # GVS adds redirect_counter parameter automatically if(!($uri =~ m/redirect_counter/) && defined($self->{_obj}->{_startFromTime}) && $self->{_obj}->{_startFromTime} > 0){ $uri .= "&begin=".$self->{_obj}->{_startFromTime}; } $self->{_uri} = $uri; $self->{_host} = $host; my $sock = Sock->instance($host)->get(); lambda { context $sock; writable { $sock->write("GET ".$uri." HTTP/1.1\r\n"); $sock->write("User-Agent: ".$self->getUserAgent()."\r\n"); $sock->write($self->{_obj}->getHeader($url)."\r\n"); } } } # Gets Uri and Host address sub getUriHost{ my $self = shift; my $url = shift || exit; my $uri = URI->new($url); my $path = $uri->path; if($uri->query){ $path .= "?".$uri->query; } return ($path,$uri->host); } # Gets UserAgent string. For IE YouTube generates diffrent HTML code, so IE is not included in the list sub getUserAgent{ my @agents = []; $agents[0] = "Mozilla/5.0 (X11; U; Linux i686; en-US) AppleWebKit/534.10 (KHTML, like Gecko) Chrome/8.0.552.224 Safari/534.10"; $agents[1] = "Mozilla/5.0 (X11; Linux i686; rv:2.0) Gecko/20110321 Firefox/4.0"; $agents[2] = "Opera/9.80 (X11; Linux i686; U; en-US) Presto/2.7.62 Version/11.01"; $agents[3] = "Mozilla/5.0 (Windows; U; Windows NT 6.0; en-US; rv:1.9.0.5) Gecko/2008120122 Firefox/3.0.5"; $agents[4] = "Mozilla/5.0 (Macintosh; U; Intel Mac OS X 10_5_6; en-us) AppleWebKit/525.27.1 (KHTML, like Gecko) Version/3.2.1 Safari/525.27.1"; return $agents[rand($#agents)]; } 1;
10. Sock.pm
Sock.pm holds IO::Socket::INET singleton object instance.
#!/usr/bin/perl package Sock; use IO::Socket; use Time::HiRes; # Create singleton instance my $singleton; # Returns Sock instance sub instance { my $class = shift; unless($singleton){ my $self = { _socket => IO::Socket::INET-> new( PeerAddr => shift || die("Host is missing"), PeerPort => 80), # Create new IO::Socket::Inet object _readBytes => 0 }; bless ($self, $class); $singleton = $self; } return $singleton; } # Reads bytes from IO::Socket::INET if can't wait 10 secs sub readBytes{ my $self = shift; my $n = shift; my $wait = shift; my $counter = shift || 10; my $buffer; my $bytes; if($wait){ if($counter > 0){ Print::warn("Waiting ".$counter); Time::HiRes::sleep(.5); $counter--; }else{ Print::err("Problem during download"); } } $bytes = read $self->{_socket}, $buffer, $n or $self->readBytes($n,1,$counter); $self->{_readBytes} += $bytes; return $buffer; } # Gets socket instance sub get{ my $self = shift; return $self->{_socket}; } # How many bytes have been read? sub bytesCount{ my $self = shift; return $self->{_readBytes}; } 1;
11. FLV.pm
FLV.pm is the core of the script. It’s resposible for stream processing (reading/creating tags, reading/sending headers etc.)
#!/usr/bin/perl package FLV; use Tag; use Sock; sub new{ my $class = shift; my $self = { _hasVideo => shift, # Enable/Disable video output _hasAudio => shift, # Enable/Disable audio output _isRawOutput => shift }; if($self->{_hasVideo} == 0 && $self->{_hasAudio} == 0){ Print::err("There's nothing to be done"); } bless $self, $class; return $self; } sub getNewFlvHeader{ my $self = shift; my $flags = (($self->{_hasAudio}) ? 0x04 : 0) | (($self->{_hasVideo}) ? 0x01 : 0); return pack 'A3CCN', 'FLV', 1, $flags, 9; } sub getTags{ my $self = shift; my $sock = Sock->instance(); # Get socket instance my ($signature, $version, $flags, $offset) = unpack 'A3CCN', $sock->readBytes(9); # Unpack header my $lastTagSize = 0; if ($signature ne 'FLV'){ Print::err("FLV signature: $signature"); } if ($version != 1){ Print::err('Internal error: I only understand FLV version 1'); } if (0 != ($flags & 0xf0)){ Print::err('Reserved header flags are non-zero at byte'); } if ($offset < 9){ Print::err('Illegal value for body offset at byte'); } if(!$self->{_isRawOutput}){ Print::out($self->getNewFlvHeader()); # Prints new FLV Header Print::out(pack 'N', 0); # First previous tag is alwas equal to 0 } while(1) { $sock->readBytes(4); # Read last tag size if(eof($sock->get())){ last; } my $tag = new Tag($self); # Create new Tag if(!$self->{_isRawOutput}){ if(($self->{_hasAudio} && !$tag->isVideo()) || ($self->{_hasVideo} && !$tag->isAudio() || ($self->{_hasVideo} && $self->{_hasAudio}))){ $tag->printTag(); $lastTagSize = $tag->getSize(); Print::out(pack 'N', $lastTagSize); # Each tag is followed by "Last Tag Size" Tag } }else{ if($self->{_hasAudio} && $tag->isAudio() || $self->{_hasVideo} && $tag->isVideo()){ $tag->{_obj}->printCodec(); # Raw output } } } } # Converts timestamp to seconds sub timestampToSeconds{ my $self = shift; my $msec = shift; return $self->round($msec/1000,2); } # Rounds up the number sub round { my $self = shift; my $number = shift || 0; my $dec = 10 ** (shift || 0); return int( $dec * $number + .5 * ($number 0)) / $dec; } 1;
12. Tag.pm
Tag.pm parse raw Tag header and then creates appropriate tag object (see AudioTag.pm, VideoTag.pm)
#!/usr/bin/perl package Tag; use AudioTag; use VideoTag; use Sock; sub new{ my $class = shift; my $self = { _flv => shift || die("FLV object is missing"), # holds FLV object _rawHeader => undef, # contains raw Tag header _type => undef, # Tag type, 8:Audio, 9:Video, 18: Meta _dataSize => undef, # Tag size _timestamp => undef, _obj => undef, # Tag object _script => undef }; bless $self, $class; $self->{_rawHeader} = Sock->instance()->readBytes(11); # Read raw tag header from stream ($self->{_type}, $self->{_dataSize}, $self->{_timestamp}) = $self->getTagInfo($self->{_rawHeader}); $self->isValid(); # Check if tag is valid if($self->{_type} == 8){ $self->{_obj} = new AudioTag($self->{_dataSize}); # Create AudioTag obj }elsif($self->{_type} == 9){ $self->{_obj} = new VideoTag($self->{_dataSize}); # Create VideoTag obj }elsif($self->{_type} == 12 || $self->{_type} == 18){ $self->{_script} = Sock->instance()->readBytes($self->{_dataSize}); # Just save raw data, we don't need to parse script or meta tag } return $self; } # Checks if current tag is an AudioTag sub isAudio{ return shift->{_type} == 8; } # Checks if current tag is an VideoTag sub isVideo{ return shift->{_type} == 9; } # Prints out the tag sub printTag{ my $self = shift; Print::out($self->{_rawHeader}); if(defined($self->{_obj})){ $self->{_obj}->printTag(); }else{ Print::tag("Meta Tag at ".Sock->instance()->bytesCount()); Print::out($self->{_script}); } } # Returns size of whole tag (header+content) sub getSize{ return shift->{_dataSize} + 11; } # Reads raw tag and returns TimeStamp, DataSize, Type sub getTagInfo{ my $self = shift; my $bytes = shift || exit; my ($type, @datasize, @timestamp); ($type, $dataSize[0], $dataSize[1], $dataSize[2], $timestamp[1], $timestamp[2], $timestamp[3], $timestamp[0]) = unpack 'CCCCCCCC', $bytes; my $dataSize = ($dataSize[0] * 256 + $dataSize[1]) * 256 + $dataSize[2]; my $timestamp = (($timestamp[0] * 256 + $timestamp[1]) * 256 + $timestamp[2]) * 256 +$timestamp[3]; return $type, $dataSize, $timestamp; } # Validates tag sub isValid{ my $self = shift; if ($self->{_timestamp} > 4_000_000_000 || $self->{_timestamp} < 0){ Print::warn("Strange timestamp: $self->{_timestamp}"); } if($self->{_dataSize} < 4){ Print::err("Tag size is too small ($self->{_dataSize})"); } } 1;
13. AudioTag.pm
AudioTag.pm holds information about audio (format, codec etc.)
#!/usr/bin/perl package AudioTag; use Sock; my $formats = { 0 => "Uncompressed", 1 => "DPCM", 2 => "MP3", 10 => "AAC" }; sub new{ my $class = shift; my $self = { _tagSize => shift || die("TagSize is missing"), _flags => shift, _rawFlags => shift, _format => undef, _rate => undef, _size => undef, _type => undef, _codecData => undef }; bless $self, $class; my $sock = Sock->instance(); $self->{_rawFlags} = $sock->readBytes(1); # Read flags $self->{_codecData} = $sock->readBytes($self->{_tagSize}-1); $self->{_flags} = unpack 'C', $self->{_rawFlags}; # Unpack. "C" - An unsigned char (octet) value. $self->{_format} = (($self->{_flags} >> 4) & 0x0f); $self->{_rate} = (($self->{_flags} >> 2) & 0x0C); $self->{_size} = (($self->{_flags} >> 1) & 0x02); $self->{_type} = (($self->{_flags} >> 0) & 0x01); return $self; } sub printCodec{ Print::out(shift->{_codecData}); } sub printTag{ my $self = shift; Print::tag("Audio Tag at ".Sock->instance()->bytesCount()." format: ".$formats->{$self->{_format}}); Print::out($self->{_rawFlags}); Print::out($self->{_codecData}); } 1;
14. VideoTag.pm
VideoTag.pm holds information about video (format, codec etc.)
#!/usr/bin/perl package VideoTag; use Sock; my $codecs = { 2 => "Sorensen H.263", 3 => "Screen video", 4 => "On2 VP6", 5 => "On2 VP6 Alpha", 6 => "ScreenVideo 2", 7 => "MPEG-4 AVC (H.264)" }; sub new{ my $class = shift; my $self = { _tagSize => shift || die("TagSize is missing"), _flags => shift, _rawFlags => shift, _codecData => undef, _codecId => undef, _frameType => undef }; bless $self, $class; my $sock = Sock->instance(); $self->{_rawFlags} = $sock->readBytes(1); $self->{_codecData} = $sock->readBytes($self->{_tagSize}-1); $self->{_flags} = unpack 'C', $self->{_rawFlags}; $self->{_codecId} = (($self->{_flags} >> 0) & 0x0f); $self->{_frameType} = (($self->{_flags} >> 4) & 0xf0); return $self; } sub printCodec{ my $self = shift; Print::out($self->{_codecData}); } sub printTag{ my $self = shift; Print::tag("Video Tag at ".Sock->instance()->bytesCount()." codec: ".$codecs->{$self->{_codecId}}); Print::out($self->{_rawFlags}); Print::out($self->{_codecData}); } 1;
15. Print.pm
Print.pm is responsible for printing out debug information/raw data.
#!/usr/bin/perl package Print; use Term::ANSIColor qw(:constants); sub err{ print STDERR BOLD, RED, "[Error]", RESET, " @_\n"; exit; } sub warn{ print STDERR BOLD, YELLOW, "[Warning]", RESET, " @_\n"; } sub tag{ print STDERR BOLD, GREEN, "\r[Tag]", RESET, " @_"; } sub info{ print STDERR BOLD, GREEN, "[Info]", RESET, " @_\n"; } sub out{ print STDOUT @_; } 1;
16. Demo
17. Download
FLV Tag extractor (tar.gz)
FLV Tag extractor (zip)