package FLV::ToSWF; use warnings; use strict; use 5.008; use SWF::File; use SWF::Element; use FLV::File; use FLV::Util; use FLV::AudioTag; use FLV::VideoTag; use English qw(-no_match_vars); use Carp; our $VERSION = '0.24'; =for stopwords SWF transcodes framerate =head1 NAME FLV::ToSWF - Convert an FLV file into a SWF file =head1 LICENSE See L =head1 SYNOPSIS use FLV::ToSwf; my $converter = FLV::ToSWF->new(); $converter->parse_flv($flv_filename); $converter->save($swf_filename); See also L. =head1 DESCRIPTION Transcodes FLV files into SWF files. See the L command-line program for a nice interface and a detailed list of caveats and limitations. =head1 METHODS =over =item $pkg->new() Instantiate a converter and prepare an empty SWF. =cut sub new { my $pkg = shift; my $self = bless { flv => FLV::File->new(), background_color => [0, 0, 0], # RGB, black }, $pkg; $self->{flv}->empty(); return $self; } =item $self->parse_flv($flv_filename) Open and parse the specified FLV file. If the FLV file lacks C details, that tag is populated with duration, framerate, video dimensions, etc. =cut sub parse_flv { my $self = shift; my $infile = shift; $self->{flv}->parse($infile); $self->{flv}->populate_meta(); $self->_validate(); return; } sub _validate { my $self = shift; my $acodec = $self->{flv}->get_meta('audiocodecid'); if (defined $acodec && $acodec != 2) { die "Audio format $AUDIO_FORMATS{$acodec} not supported; " . "only MP3 audio allowed\n"; } return; } =item $self->save($swf_filename) Write out a SWF file. Note: this is usually called only after C. Throws an exception upon error. =cut sub save { my $self = shift; my $outfile = shift; # Collect FLV info my $flvinfo = $self->_flvinfo(); # Create a new SWF my $swf = $self->_startswf($flvinfo); $self->{audsamples} = 0; for my $i (0 .. $#{ $flvinfo->{vidtags} }) { my $vidtag = $flvinfo->{vidtags}->[$i]; my $data = $vidtag->{data}; if (4 == $vidtag->{codec} || 5 == $vidtag->{codec}) { # On2 VP6 is different in FLV vs. SWF! if ($data !~ s/\A(.)//xms || $1 ne pack 'C', 0) { warn 'This FLV has a non-zero video size adjustment. ' . "It may not play properly as a SWF...\n"; } } SWF::Element::Tag::VideoFrame->new( StreamID => 1, FrameNum => $i, VideoData => $data, )->pack($swf); if (0 == $i) { SWF::Element::Tag::PlaceObject2->new( Flags => 22, # matrix, tween ratio and characterID CharacterID => 1, Matrix => SWF::Element::MATRIX->new( ScaleX => 1, ScaleY => 1, RotateSkew0 => 0, RotateSkew1 => 0, TranslateX => 0, TranslateY => 0, ), Ratio => $i, Depth => 4, )->pack($swf); } else { SWF::Element::Tag::PlaceObject2->new( Flags => 17, # move and tween ratio Ratio => $i, Depth => 4, )->pack($swf); } $self->_add_audio($swf, $flvinfo, $vidtag->{start}, $i == $#{ $flvinfo->{vidtags} }); SWF::Element::Tag::ShowFrame->new()->pack($swf); } # Save to disk $swf->close(q{-} eq $outfile ? \*STDOUT : $outfile); return; } sub _add_audio { my $self = shift; my $swf = shift; my $flvinfo = shift; my $start = shift; my $islast = shift; if (@{ $flvinfo->{audtags} }) { my $data = q{}; my $any_tag = $flvinfo->{audtags}->[0]; my $audstart = $any_tag->{start}; my $format = $any_tag->{format}; my $stereo = $any_tag->{type}; my $ratecode = $any_tag->{rate}; if ($format != 2) { die 'Only MP3 audio supported so far...'; } (my $rate = $AUDIO_RATES{$ratecode}) =~ s/\D//gxms; my $bytes_per_sample = ($stereo ? 2 : 1) * ($any_tag->{size} ? 2 : 1); my $needsamples = int 0.001 * $start * $rate; my $startsamples = $self->{audsamples}; while (@{ $flvinfo->{audtags} } && ($islast || $self->{audsamples} < $needsamples)) { my $atag = shift @{ $flvinfo->{audtags} }; $data .= $atag->{data}; $self->{audsamples} = $self->_round_to_samples( @{ $flvinfo->{audtags} } ? 0.001 * $flvinfo->{audtags}->[0]->{start} * $rate : 1_000_000_000 ); } if (0 < length $data) { my $samples = $self->{audsamples} - $startsamples; my $seek = $startsamples ? int $needsamples - $startsamples : 0; # signed -> unsigned conversion $seek = unpack 'S', pack 's', $seek; my $head = pack 'vv', $samples, $seek; SWF::Element::Tag::SoundStreamBlock->new( StreamSoundData => $head . $data)->pack($swf); } } return; } sub _flvinfo { my $self = shift; my %flvinfo = ( duration => $self->{flv}->get_meta('duration') || 0, vcodec => $self->{flv}->get_meta('videocodecid') || 0, acodec => $self->{flv}->get_meta('audiocodecid') || 0, width => $self->{flv}->get_meta('width') || 320, height => $self->{flv}->get_meta('height') || 240, framerate => $self->{flv}->get_meta('framerate') || 12, vidbytes => 0, audbytes => 0, vidtags => [], audtags => [], ); $flvinfo{swfversion} = $flvinfo{vcodec} >= 4 ? 8 : 6; if ($self->{flv}->{body}) { for my $tag ($self->{flv}->{body}->get_tags()) { if ($tag->isa('FLV::VideoTag')) { push @{ $flvinfo{vidtags} }, $tag; $flvinfo{vidbytes} += length $tag->{data}; } elsif ($tag->isa('FLV::AudioTag')) { push @{ $flvinfo{audtags} }, $tag; $flvinfo{audbytes} += length $tag->{data}; } } } return \%flvinfo; } sub _startswf { my $self = shift; my $flvinfo = shift; # SWF header my $twp = 20; # 20 twips per pixel my $swf = SWF::File->new( undef, Version => $flvinfo->{swfversion}, FrameSize => [0, 0, $twp * $flvinfo->{width}, $twp * $flvinfo->{height}], FrameRate => $flvinfo->{framerate}, ); ## Populate the SWF # Generic stuff... my $bg = $self->{background_color}; SWF::Element::Tag::SetBackgroundColor->new( BackgroundColor => [ Red => $bg->[0], Green => $bg->[1], Blue => $bg->[2], ], )->pack($swf); # Add the audio stream header if (@{ $flvinfo->{audtags} }) { my $tag = $flvinfo->{audtags}->[0]; (my $arate = $AUDIO_RATES{ $tag->{rate} }) =~ s/\D//gxms; SWF::Element::Tag::SoundStreamHead->new( StreamSoundCompression => $tag->{format}, PlaybackSoundRate => $tag->{rate}, StreamSoundRate => $tag->{rate}, PlaybackSoundSize => $tag->{size}, StreamSoundSize => $tag->{size}, PlaybackSoundType => $tag->{type}, StreamSoundType => $tag->{type}, StreamSoundSampleCount => $arate / $flvinfo->{framerate}, )->pack($swf); } # Add the video stream header if (@{ $flvinfo->{vidtags} }) { my $tag = $flvinfo->{vidtags}->[0]; SWF::Element::Tag::DefineVideoStream->new( CharacterID => 1, NumFrames => scalar @{ $flvinfo->{vidtags} }, Width => $flvinfo->{width}, Height => $flvinfo->{height}, VideoFlags => 1, # Smoothing on CodecID => $tag->{codec}, )->pack($swf); } return $swf; } sub _round_to_samples { my $pkg_or_self = shift; my $samples = shift; return 576 * int $samples / 576 + 0.5; } 1; __END__ =back =head1 AUTHOR See L =cut