1package FLV::ToSWF;
2
3use warnings;
4use strict;
5use 5.008;
6
7use SWF::File;
8use SWF::Element;
9use FLV::File;
10use FLV::Util;
11use FLV::AudioTag;
12use FLV::VideoTag;
13use English qw(-no_match_vars);
14use Carp;
15
16our $VERSION = '0.24';
17
18=for stopwords SWF transcodes framerate
19
20=head1 NAME
21
22FLV::ToSWF - Convert an FLV file into a SWF file
23
24=head1 LICENSE
25
26See L<FLV::Info>
27
28=head1 SYNOPSIS
29
30   use FLV::ToSwf;
31   my $converter = FLV::ToSWF->new();
32   $converter->parse_flv($flv_filename);
33   $converter->save($swf_filename);
34
35See also L<flv2swf>.
36
37=head1 DESCRIPTION
38
39Transcodes FLV files into SWF files.  See the L<flv2swf> command-line
40program for a nice interface and a detailed list of caveats and
41limitations.
42
43=head1 METHODS
44
45=over
46
47=item $pkg->new()
48
49Instantiate a converter and prepare an empty SWF.
50
51=cut
52
53sub new
54{
55   my $pkg = shift;
56
57   my $self = bless {
58      flv              => FLV::File->new(),
59      background_color => [0, 0, 0],          # RGB, black
60   }, $pkg;
61   $self->{flv}->empty();
62   return $self;
63}
64
65=item $self->parse_flv($flv_filename)
66
67Open and parse the specified FLV file.  If the FLV file lacks
68C<onMetadata> details, that tag is populated with duration,
69framerate, video dimensions, etc.
70
71=cut
72
73sub parse_flv
74{
75   my $self   = shift;
76   my $infile = shift;
77
78   $self->{flv}->parse($infile);
79   $self->{flv}->populate_meta();
80
81   $self->_validate();
82
83   return;
84}
85
86sub _validate
87{
88   my $self = shift;
89
90   my $acodec = $self->{flv}->get_meta('audiocodecid');
91   if (defined $acodec && $acodec != 2)
92   {
93      die "Audio format $AUDIO_FORMATS{$acodec} not supported; "
94          . "only MP3 audio allowed\n";
95   }
96   return;
97}
98
99=item $self->save($swf_filename)
100
101Write out a SWF file.  Note: this is usually called only after
102C<parse_flv()>.  Throws an exception upon error.
103
104=cut
105
106sub save
107{
108   my $self    = shift;
109   my $outfile = shift;
110
111   # Collect FLV info
112   my $flvinfo = $self->_flvinfo();
113
114   # Create a new SWF
115   my $swf = $self->_startswf($flvinfo);
116
117   $self->{audsamples} = 0;
118
119   for my $i (0 .. $#{ $flvinfo->{vidtags} })
120   {
121      my $vidtag = $flvinfo->{vidtags}->[$i];
122      my $data   = $vidtag->{data};
123      if (4 == $vidtag->{codec} || 5 == $vidtag->{codec})
124      {
125
126         # On2 VP6 is different in FLV vs. SWF!
127         if ($data !~ s/\A(.)//xms || $1 ne pack 'C', 0)
128         {
129            warn 'This FLV has a non-zero video size adjustment. '
130                . "It may not play properly as a SWF...\n";
131         }
132      }
133      SWF::Element::Tag::VideoFrame->new(
134         StreamID  => 1,
135         FrameNum  => $i,
136         VideoData => $data,
137      )->pack($swf);
138
139      if (0 == $i)
140      {
141         SWF::Element::Tag::PlaceObject2->new(
142            Flags       => 22,    # matrix, tween ratio and characterID
143            CharacterID => 1,
144            Matrix => SWF::Element::MATRIX->new(
145               ScaleX      => 1,
146               ScaleY      => 1,
147               RotateSkew0 => 0,
148               RotateSkew1 => 0,
149               TranslateX  => 0,
150               TranslateY  => 0,
151            ),
152            Ratio => $i,
153            Depth => 4,
154         )->pack($swf);
155      }
156      else
157      {
158         SWF::Element::Tag::PlaceObject2->new(
159            Flags => 17,    # move and tween ratio
160            Ratio => $i,
161            Depth => 4,
162         )->pack($swf);
163      }
164
165      $self->_add_audio($swf, $flvinfo, $vidtag->{start},
166         $i == $#{ $flvinfo->{vidtags} });
167
168      SWF::Element::Tag::ShowFrame->new()->pack($swf);
169   }
170
171   # Save to disk
172   $swf->close(q{-} eq $outfile ? \*STDOUT : $outfile);
173
174   return;
175}
176
177sub _add_audio
178{
179   my $self    = shift;
180   my $swf     = shift;
181   my $flvinfo = shift;
182   my $start   = shift;
183   my $islast  = shift;
184
185   if (@{ $flvinfo->{audtags} })
186   {
187      my $data     = q{};
188      my $any_tag  = $flvinfo->{audtags}->[0];
189      my $audstart = $any_tag->{start};
190      my $format   = $any_tag->{format};
191      my $stereo   = $any_tag->{type};
192      my $ratecode = $any_tag->{rate};
193
194      if ($format != 2)
195      {
196         die 'Only MP3 audio supported so far...';
197      }
198
199      (my $rate = $AUDIO_RATES{$ratecode}) =~ s/\D//gxms;
200      my $bytes_per_sample = ($stereo ? 2 : 1) * ($any_tag->{size} ? 2 : 1);
201
202      my $needsamples  = int 0.001 * $start * $rate;
203      my $startsamples = $self->{audsamples};
204
205      while (@{ $flvinfo->{audtags} }
206         && ($islast || $self->{audsamples} < $needsamples))
207      {
208         my $atag = shift @{ $flvinfo->{audtags} };
209         $data .= $atag->{data};
210         $self->{audsamples} = $self->_round_to_samples(
211            @{ $flvinfo->{audtags} }
212            ? 0.001 * $flvinfo->{audtags}->[0]->{start} * $rate
213            : 1_000_000_000
214         );
215      }
216      if (0 < length $data)
217      {
218         my $samples = $self->{audsamples} - $startsamples;
219
220         my $seek = $startsamples ? int $needsamples - $startsamples : 0;
221
222         # signed -> unsigned conversion
223         $seek = unpack 'S', pack 's', $seek;
224
225         my $head = pack 'vv', $samples, $seek;
226         SWF::Element::Tag::SoundStreamBlock->new(
227            StreamSoundData => $head . $data)->pack($swf);
228      }
229   }
230   return;
231}
232
233sub _flvinfo
234{
235   my $self = shift;
236
237   my %flvinfo = (
238      duration  => $self->{flv}->get_meta('duration')     || 0,
239      vcodec    => $self->{flv}->get_meta('videocodecid') || 0,
240      acodec    => $self->{flv}->get_meta('audiocodecid') || 0,
241      width     => $self->{flv}->get_meta('width')        || 320,
242      height    => $self->{flv}->get_meta('height')       || 240,
243      framerate => $self->{flv}->get_meta('framerate')    || 12,
244      vidbytes  => 0,
245      audbytes  => 0,
246      vidtags   => [],
247      audtags   => [],
248   );
249   $flvinfo{swfversion} = $flvinfo{vcodec} >= 4 ? 8 : 6;
250
251   if ($self->{flv}->{body})
252   {
253      for my $tag ($self->{flv}->{body}->get_tags())
254      {
255         if ($tag->isa('FLV::VideoTag'))
256         {
257            push @{ $flvinfo{vidtags} }, $tag;
258            $flvinfo{vidbytes} += length $tag->{data};
259         }
260         elsif ($tag->isa('FLV::AudioTag'))
261         {
262            push @{ $flvinfo{audtags} }, $tag;
263            $flvinfo{audbytes} += length $tag->{data};
264         }
265      }
266   }
267
268   return \%flvinfo;
269}
270
271sub _startswf
272{
273   my $self    = shift;
274   my $flvinfo = shift;
275
276   # SWF header
277   my $twp = 20;               # 20 twips per pixel
278   my $swf = SWF::File->new(
279      undef,
280      Version => $flvinfo->{swfversion},
281      FrameSize =>
282          [0, 0, $twp * $flvinfo->{width}, $twp * $flvinfo->{height}],
283      FrameRate => $flvinfo->{framerate},
284   );
285
286   ## Populate the SWF
287
288   # Generic stuff...
289   my $bg = $self->{background_color};
290   SWF::Element::Tag::SetBackgroundColor->new(
291      BackgroundColor => [
292         Red   => $bg->[0],
293         Green => $bg->[1],
294         Blue  => $bg->[2],
295      ],
296   )->pack($swf);
297
298   # Add the audio stream header
299   if (@{ $flvinfo->{audtags} })
300   {
301      my $tag = $flvinfo->{audtags}->[0];
302      (my $arate = $AUDIO_RATES{ $tag->{rate} }) =~ s/\D//gxms;
303      SWF::Element::Tag::SoundStreamHead->new(
304         StreamSoundCompression => $tag->{format},
305         PlaybackSoundRate      => $tag->{rate},
306         StreamSoundRate        => $tag->{rate},
307         PlaybackSoundSize      => $tag->{size},
308         StreamSoundSize        => $tag->{size},
309         PlaybackSoundType      => $tag->{type},
310         StreamSoundType        => $tag->{type},
311         StreamSoundSampleCount => $arate / $flvinfo->{framerate},
312      )->pack($swf);
313   }
314
315   # Add the video stream header
316   if (@{ $flvinfo->{vidtags} })
317   {
318      my $tag = $flvinfo->{vidtags}->[0];
319      SWF::Element::Tag::DefineVideoStream->new(
320         CharacterID => 1,
321         NumFrames   => scalar @{ $flvinfo->{vidtags} },
322         Width       => $flvinfo->{width},
323         Height      => $flvinfo->{height},
324         VideoFlags  => 1,                                 # Smoothing on
325         CodecID     => $tag->{codec},
326      )->pack($swf);
327   }
328
329   return $swf;
330}
331
332sub _round_to_samples
333{
334   my $pkg_or_self = shift;
335   my $samples     = shift;
336
337   return 576 * int $samples / 576 + 0.5;
338}
339
3401;
341
342__END__
343
344=back
345
346=head1 AUTHOR
347
348See L<FLV::Info>
349
350=cut
351