The Perl Toolchain Summit needs more sponsors. If your company depends on Perl, please support this very important event.
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<FLV::Info>

=head1 SYNOPSIS

   use FLV::ToSwf;
   my $converter = FLV::ToSWF->new();
   $converter->parse_flv($flv_filename);
   $converter->save($swf_filename);

See also L<flv2swf>.

=head1 DESCRIPTION

Transcodes FLV files into SWF files.  See the L<flv2swf> 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<onMetadata> 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<parse_flv()>.  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<FLV::Info>

=cut