File Coverage

blib/lib/FLV/Body.pm
Criterion Covered Total %
statement 207 234 88.4
branch 33 56 58.9
condition 13 20 65.0
subroutine 27 29 93.1
pod 15 15 100.0
total 295 354 83.3


line stmt bran cond sub pod time code
1             package FLV::Body;
2              
3 6     6   36 use warnings;
  6         10  
  6         289  
4 6     6   33 use strict;
  6         10  
  6         328  
5 6     6   112 use 5.008;
  6         20  
  6         225  
6 6     6   39 use Carp;
  6         12  
  6         773  
7 6     6   36 use English qw(-no_match_vars);
  6         18  
  6         71  
8 6     6   4066 use File::Temp qw();
  6         11  
  6         171  
9              
10 6     6   87 use base 'FLV::Base';
  6         13  
  6         755  
11              
12 6     6   33 use FLV::Header;
  6         10  
  6         423  
13 6     6   6420 use FLV::Tag;
  6         19  
  6         167  
14 6     6   51 use FLV::VideoTag;
  6         14  
  6         143  
15 6     6   31 use FLV::AudioTag;
  6         14  
  6         192  
16 6     6   35 use FLV::MetaTag;
  6         15  
  6         28158  
17              
18             our $VERSION = '0.24';
19              
20             =for stopwords keyframe zeroth
21              
22             =head1 NAME
23              
24             FLV::Body - Flash video file data structure
25              
26             =head1 LICENSE
27              
28             See L
29              
30             =head1 METHODS
31              
32             This is a subclass of FLV::Base.
33              
34             =over
35              
36             =item $self->parse($fileinst)
37              
38             Takes a FLV::File instance and extracts the FLV body from the file
39             stream. This method throws exceptions if the stream is not a valid
40             FLV v1.0 or v1.1 file.
41              
42             There is no return value.
43              
44             =cut
45              
46             sub parse
47             {
48 32     32 1 73 my $self = shift;
49 32         55 my $file = shift;
50 32         53 my $opts = shift;
51 32   50     107 $opts ||= {};
52              
53 32         54 my @tags;
54              
55             TAGS:
56 32         61 while (1)
57             {
58 16281         45114 my $lastsize = $file->get_bytes(4);
59              
60 16280 100       44675 if ($file->at_end())
61             {
62 31         129 last TAGS;
63             }
64              
65 16249         48800 my $tag = FLV::Tag->new();
66 16249         51199 $tag->parse($file, $opts); # might throw exception
67 16249         43510 push @tags, $tag->get_payload();
68             }
69              
70 31         192 my %tagorder = (
71             'FLV::MetaTag' => 1,
72             'FLV::AudioTag' => 2,
73             'FLV::VideoTag' => 3,
74             );
75 19612 50       75476 @tags = sort {
76 31         907 $a->{start} <=> $b->{start}
77             || $tagorder{ ref $a } <=> $tagorder{ ref $b }
78             } @tags;
79 31         185 $self->{tags} = \@tags;
80 31         188 return;
81             }
82              
83             =item $self->clone()
84              
85             Create an independent copy of this instance.
86              
87             =cut
88              
89             sub clone
90             {
91 8     8 1 18 my $self = shift;
92              
93 8         38 my $copy = FLV::Body->new;
94 8         19 $copy->{tags} = [ map { $_->clone } @{$self->{tags}} ];
  3480         9657  
  8         47  
95 8         171 return $copy;
96             }
97              
98             =item $self->serialize($filehandle)
99              
100             Serializes the in-memory FLV body. If that representation is not
101             complete, this throws an exception via croak(). Returns a boolean
102             indicating whether writing to the file handle was successful.
103              
104             =cut
105              
106             sub serialize
107             {
108 17     17 1 38 my $self = shift;
109 17   33     63 my $filehandle = shift || croak 'Please specify a filehandle';
110 17   50     62 my $headersize = shift || 9;
111              
112 17 50       38 return if (!print {$filehandle} pack 'V', 0);
  17         109  
113 17 50       97 return if (!$self->{tags});
114              
115 17         37 my $size_so_far = $headersize + 4;
116 17         38 for my $i (0 .. $#{ $self->{tags} })
  17         73  
117             {
118 1479         3505 my $tag = $self->{tags}->[$i];
119 1479 100 66     11295 if (
      66        
120             $tag->isa('FLV::MetaTag')
121             && ( defined $tag->get_value('keyframes')
122             || defined $tag->get_value('filesize'))
123             )
124             {
125 13         71 return $self->_serialize_with_sizes($filehandle, $i, $size_so_far);
126             }
127 1466         5499 my $size = FLV::Tag->serialize($tag, $filehandle);
128 1466 50       3368 if (!$size)
129             {
130 0         0 return;
131             }
132 1466         2608 print {$filehandle} pack 'V', $size;
  1466         4940  
133 1466         3832 $size_so_far += $size + 4;
134             }
135 4         44 return 1;
136             }
137              
138             sub _serialize_with_sizes
139             {
140 13     13   25 my $self = shift;
141 13         27 my $filehandle = shift;
142 13         26 my $i = shift;
143 13         22 my $size_so_far = shift;
144              
145 13         36 my $meta = $self->{tags}->[$i];
146              
147 13         45 my $keyframes = $meta->get_value('keyframes');
148 13         51 my $filesize = $meta->get_value('filesize');
149              
150             # Write the REST of the tags out to a tempfile
151 13         90 my ($media_fh, $media_filename) = File::Temp::tempfile();
152 13         8133 my $success = 1;
153 13         29 my $pos = 0;
154 13         23 my @filepositions;
155 13         50 for my $tag (@{ $self->{tags} }[$i + 1 .. $#{ $self->{tags} }])
  13         171  
  13         362  
156             {
157 7694 100 100     43514 if ($tag->isa('FLV::VideoTag') && $tag->is_keyframe())
158             {
159 72         189 push @filepositions, $pos;
160             }
161 7694         24154 my $size = FLV::Tag->serialize($tag, $media_fh);
162 7694 50       15032 if (!$size)
163             {
164 0         0 $success = 0;
165 0         0 last;
166             }
167 7694         8228 print {$media_fh} pack 'V', $size;
  7694         15889  
168 7694         14898 $pos += $size + 4;
169             }
170 13 50       8356 close $media_fh or warn 'Unexpected error closing filehandle';
171              
172 13 50       57 if (!$success)
173             {
174              
175             # Abort, write out without file positions
176 0         0 delete $keyframes->{filepositions};
177 0         0 $meta->set_value('filesize', undef);
178 0         0 my $size = FLV::Tag->serialize($meta, $filehandle);
179 0 0       0 if (!$size)
180             {
181 0         0 unlink $media_filename;
182 0         0 return;
183             }
184 0         0 print {$filehandle} pack 'V', $size;
  0         0  
185 0         0 $self->_copy_file_to_fh($media_filename, $filehandle);
186 0         0 unlink $media_filename;
187 0         0 return;
188             }
189              
190             # Problem: changing the file positions in the metatag changes the
191             # size of the metatag and, thus, the filepositions.
192              
193             # Solution: set file positions in metadata, write out as temp file
194             # to get resulting size, and iterate until sizes converge. This
195             # should happen on the second iteration if the sizes are written
196             # out as numbers and not as strings.
197              
198             # Start with a (wrong) guess of zero bytes
199 13         100 my ($meta_fh, $meta_filename) = File::Temp::tempfile();
200 13 50       10408 close $meta_fh or warn 'Unexpected error closing filehandle';
201              
202 13         101 my $tries = 0;
203 13         58 while ($tries++ < 10)
204             {
205 26         431 my $meta_size = -s $meta_filename;
206              
207             # Put in corrected sizes
208 26         52 my $offset = $size_so_far + $meta_size;
209 26 50       82 if ($keyframes)
210             {
211 26         69 $keyframes->{filepositions} = [map { $offset + $_ } @filepositions];
  144         314  
212             }
213 26         658 $meta->set_value('filesize', $offset + -s $media_filename);
214              
215             # Write out meta tag to tempfile
216             # Warning: I'm ignoring the case of a failure to write out the
217             # metatag at all
218 26         105 my ($try_fh, $try_filename) = File::Temp::tempfile();
219 26         10202 my $size = FLV::Tag->serialize($meta, $try_fh);
220 26 50       97 if ($size)
221             {
222 26         45 print {$try_fh} pack 'V', $size;
  26         101  
223             }
224 26 50       1859 close $try_fh or warn 'Unexpected error closing filehandle';
225              
226             # Clean up last try. This try becomes "last try" for the next iteration
227 26         3520 unlink $meta_filename;
228 26         62 $meta_filename = $try_filename;
229              
230             # Did we converge?
231 26 100       577 if ($meta_size == -s $meta_filename)
232             {
233              
234             # Yes!
235 13         90 last;
236             }
237              
238             # Otherwise do another iteration
239             }
240              
241 13         79 $self->_copy_file_to_fh($meta_filename, $filehandle);
242 13         1145 unlink $meta_filename;
243 13         45 $self->_copy_file_to_fh($media_filename, $filehandle);
244 13         4718 unlink $media_filename;
245 13         251 return 1;
246             }
247              
248             sub _copy_file_to_fh
249             {
250 26     26   47 my $self = shift;
251 26         54 my $filename = shift;
252 26         41 my $filehandle = shift;
253              
254 26 50       1151 open my $fh, '<', $filename or die 'Failed to open temporary file';
255 26 50       98 binmode $fh or die 'Failed to set binary mode on file';
256 26         43 my $buf;
257 26         426 while (read $fh, $buf, 4096)
258             {
259 1320         1721 print {$filehandle} $buf;
  1320         40149  
260             }
261 26 50       430 close $fh or warn 'Unexpected error closing filehandle';
262 26         174 return;
263             }
264              
265             =item $self->get_info()
266              
267             Returns a hash of FLV metadata. See File::Info for more details.
268              
269             =cut
270              
271             sub get_info
272             {
273 4     4 1 6 my $self = shift;
274              
275 1740         5501 my %info = (
276             duration => $self->last_start_time(),
277             FLV::VideoTag->get_info(
278 4         24 grep { $_->isa('FLV::VideoTag') } @{ $self->{tags} }
  1740         5179  
279             ),
280             FLV::AudioTag->get_info(
281 4         23 grep { $_->isa('FLV::AudioTag') } @{ $self->{tags} }
  1740         6946  
282             ),
283             FLV::MetaTag->get_info(
284 4         21 grep { $_->isa('FLV::MetaTag') } @{ $self->{tags} }
  4         27  
285             ),
286             );
287              
288 4         182 return %info;
289             }
290              
291             =item $self->get_tags()
292              
293             Returns an array of tag instances.
294              
295             =cut
296              
297             sub get_tags
298             {
299 77     77 1 171 my $self = shift;
300              
301 77 100       114 return @{ $self->{tags} || [] };
  77         3718  
302             }
303              
304             =item $self->set_tags(@tags)
305              
306             Replace all of the existing tags with new ones. For example, you can
307             remove all audio from a movie like so:
308              
309             $body->set_tags(grep {!$_->isa('FLV::AudioTag')} $body->get_tags);
310              
311             =cut
312              
313             sub set_tags
314             {
315 0     0 1 0 my $self = shift;
316 0         0 my @tags = @_;
317 0         0 $self->{tags} = \@tags;
318 0         0 return;
319             }
320              
321             =item $self->get_video_frames()
322              
323             Returns the video tags (FLV::VideoTag instances) in the FLV stream.
324              
325             =cut
326              
327             sub get_video_frames
328             {
329 28     28 1 48 my $self = shift;
330              
331 28         52 return grep { $_->isa('FLV::VideoTag') } @{ $self->{tags} };
  17160         65753  
  28         150  
332             }
333              
334             =item $self->get_video_keyframes()
335              
336             Returns just the video tags which contain keyframe data.
337              
338             =cut
339              
340             sub get_video_keyframes
341             {
342 2     2 1 5 my $self = shift;
343              
344             return
345 870 100       3985 grep { $_->isa('FLV::VideoTag') && $_->is_keyframe() }
  2         10  
346 2         2 @{ $self->{tags} };
347             }
348              
349             =item $self->get_audio_packets()
350              
351             Returns the audio tags (FLV::AudioTag instances) in the FLV stream.
352              
353             =cut
354              
355             sub get_audio_packets
356             {
357 15     15 1 32 my $self = shift;
358              
359 15         25 return grep { $_->isa('FLV::AudioTag') } @{ $self->{tags} };
  9015         35383  
  15         79  
360             }
361              
362             =item $self->get_meta_tags()
363              
364             Returns the meta tags (FLV::MetaTag instances) in the FLV stream.
365              
366             =cut
367              
368             sub get_meta_tags
369             {
370 10     10 1 25 my $self = shift;
371              
372 10         17 return grep { $_->isa('FLV::MetaTag') } @{ $self->{tags} };
  3802         14228  
  10         42  
373             }
374              
375             =item $self->last_start_time()
376              
377             Returns the start timestamp of the last tag, in milliseconds.
378              
379             =cut
380              
381             sub last_start_time
382             {
383 5     5 1 12 my $self = shift;
384              
385 5 50       27 my $tag = $self->{tags}->[-1]
386             or die 'No tags found';
387 5         24 return $tag->{start};
388             }
389              
390             =item $self->get_meta($key);
391              
392             =item $self->set_meta($key, $value, ...);
393              
394             These are convenience functions for interacting with an C
395             tag at time 0, which is a common convention in FLV files. If the zeroth
396             tag is not an L instance, one is created and prepended
397             to the tag list.
398              
399             See also C and C in L.
400              
401             =cut
402              
403             sub get_meta
404             {
405 70     70 1 96 my $self = shift;
406 70         94 my $key = shift;
407              
408 70 50       191 return if (!$self->{tags});
409 70         95 for my $meta (grep { $_->isa('FLV::MetaTag') } @{ $self->{tags} })
  26294         108746  
  70         264  
410             {
411 70         299 my $value = $meta->get_value($key);
412 70 50       725 return $value if (defined $value);
413             }
414 0         0 return;
415             }
416              
417             sub set_meta
418             {
419 75     75 1 321 my ($self, @keyvalues) = @_;
420              
421 75   100     270 $self->{tags} ||= [];
422 75         130 my @metatags = grep { $_->isa('FLV::MetaTag') } @{ $self->{tags} };
  21882         97995  
  75         306  
423 75 100       279 if (!@metatags)
424             {
425              
426             # no metatags at all! Create one.
427 23         222 my $new_meta = FLV::MetaTag->new();
428 23         113 $new_meta->{start} = 0;
429 23         58 unshift @{ $self->{tags} }, $new_meta;
  23         326  
430 23         68 @metatags = ($new_meta);
431             }
432              
433             KEYVALUE:
434 75         261 while (@keyvalues)
435             {
436 445         1197 my ($key, $value) = splice @keyvalues, 0, 2;
437              
438             # Check all existing meta tags for that key
439 445         839 for my $meta (@metatags)
440             {
441 445 100       1416 if (defined $meta->get_value($key))
442             {
443 94         253 $meta->set_value($key => $value);
444 94         298 next KEYVALUE;
445             }
446             }
447              
448             # key not found
449 351         1513 $metatags[0]->set_value($key => $value);
450             }
451              
452 75         214 return;
453             }
454              
455             =item $self->merge_meta()
456              
457             Consolidate zero or more meta tags into a single tag. If there are
458             more than one tags and there are any duplicate keys, the first key
459             takes precedence.
460              
461             =cut
462              
463             sub merge_meta
464             {
465 17     17 1 40 my $self = shift;
466              
467 17   50     72 $self->{tags} ||= [];
468              
469             # Remove all meta tags
470 17         37 my @meta = grep { $_->isa('FLV::MetaTag') } @{ $self->{tags} };
  9448         42582  
  17         107  
471 17         44 @{ $self->{tags} } = grep { !$_->isa('FLV::MetaTag') } @{ $self->{tags} };
  17         6751  
  9448         36707  
  17         95  
472              
473             # Merge all metadata
474 17         337 my %meta = map { $_->get_values() } reverse @meta;
  18         106  
475              
476             # Insert a new metatag
477 17         161 $self->set_meta(%meta);
478 17         217 return;
479             }
480              
481             =item $self->make_header()
482              
483             Create a new header from the body data.
484              
485             =cut
486              
487             sub make_header
488             {
489 0     0 1   my $self = shift;
490 0           my $header = FLV::Header->new;
491              
492 0           for my $tag (@{$self->{tags}})
  0            
493             {
494 0 0         if ($tag->isa('FLV::VideoTag'))
    0          
495             {
496 0           $header->{has_video} = 1;
497             }
498             elsif ($tag->isa('FLV::AudioTag'))
499             {
500 0           $header->{has_audio} = 1;
501             }
502             }
503 0           return $header;
504             }
505              
506             1;
507              
508             __END__