package FLV::Cut;
use warnings;
use strict;
use 5.008;
use FLV::File;
use FLV::Util;
use List::MoreUtils qw(any);
use English qw(-no_match_vars);
use Carp;
use Readonly;
our $VERSION = '0.24';
=for stopwords FLVs undef keyframes keyframe
=head1 NAME
FLV::Cut - Extract FLV segments into new files
=head1 LICENSE
See L<FLV::Info>
=head1 SYNOPSIS
use FLV::Cut;
my $converter = FLV::Cut->new();
$converter->add_output('first_ten_sec.flv', undef, 10_000);
$converter->add_output('middle_ten_sec.flv', 20_000, 30_000);
$converter->add_output('tail.flv', 40_000, undef);
$converter->parse_flv('input.flv');
$converter->save_all;
=head1 DESCRIPTION
Efficiently extracts segments of an FLV into separate files.
WARNING: this tool does not help you find keyframes! If you pick a
cut time that is not on a keyframe, you will see unpleasant video
artifacts in the resulting media. For many input FLVs, you can use
the following to find the keyframe times:
my $flv = FLV::File->new;
$flv->parse;
$flv->populate_meta; # optional
my @times = @{ $flv->get_meta('keyframe')->{times} };
=head1 METHODS
=over
=item $pkg->new()
Instantiate a converter.
=cut
Readonly::Scalar my $MAX_TIME => 4_000_000_000;
sub new
{
my $pkg = shift;
my $start = { time => 0, out => [] };
my $end = { time => $MAX_TIME + 1, out => [] };
my $self = bless {
times => [$start, $end], ## no critic (Comma)
outfiles => {},
}, $pkg;
return $self;
}
=item $self->add_output($flv_filename, $in_milliseconds, $out_milliseconds)
Register an output file for a particular time slice. Either the in or
out times can be undefined to imply the beginning or end of the FLV,
respectively. You can set both to undef, but that's pretty
pointless... If the in or out times are not represented in the input
FLV, that's OK -- you may just end up with less data than you expected
in the output files.
=cut
sub add_output
{
my $self = shift;
my $outfile = shift;
my $cutin = shift || 0;
my $cutout = shift || $MAX_TIME;
if ($cutin < 0 || $cutout < 0)
{
croak 'Illegal negative time';
}
if ($cutin > $MAX_TIME || $cutout > $MAX_TIME)
{
croak 'Illegal huge time';
}
if ($cutin >= $cutout)
{
carp 'Ignoring cut-in >= cut-out';
return;
}
my $out = FLV::File->new();
$out->empty();
my $outfh = FLV::Util->get_write_filehandle($outfile);
if (!$outfh)
{
die 'Failed to write FLV file: ' . $OS_ERROR;
}
if ($self->{outfiles}->{$outfile})
{
if ($self->{outfiles}->{$outfile}->{cutin} > $cutin)
{
$self->{outfiles}->{$outfile}->{cutin} = $cutin;
}
}
else
{
$self->{outfiles}->{$outfile}
= { flv => $out, fh => $outfh, cutin => $cutin };
}
my $times = $self->{times};
my $i = 0;
while ($times->[$i]->{time} < $cutin)
{
++$i;
}
if ($times->[$i]->{time} != $cutin)
{
# A new cutin time
my $new_time
= { time => $cutin, out => [@{ $times->[$i - 1]->{out} }] };
splice @{$times}, $i, 0, $new_time;
}
my $added_last;
while ($times->[$i]->{time} <= $cutout)
{
if (any { $_ eq $outfile } @{ $times->[$i]->{out} })
{
$added_last = undef;
}
else
{
$added_last = 1;
push @{ $times->[$i]->{out} }, $outfile;
}
++$i;
}
if ($times->[$i]->{time} != $cutout + 1)
{
# A new cutout time
my @out = @{ $times->[$i - 1]->{out} };
# It should not include this $outfile, unless it previously
# spanned this cutout
if ($added_last)
{
pop @out;
}
splice @{$times}, $i, 0, { time => $cutout + 1, out => \@out };
}
return;
}
=item $self->parse_flv($flv_filename)
=item $self->parse_flv($flv_instance)
Open and parse the specified FLV file. Alternatively, you may pass an
instantiated and parsed L<FLV::File> object.
=cut
sub parse_flv
{
my $self = shift;
my $infile = shift;
my $flv;
if (ref $infile && $infile->isa('FLV::File'))
{
$flv = $infile;
}
else
{
$flv = FLV::File->new;
$flv->parse($infile);
}
# ASSUMPTION: tags are time-sorted
my $times = $self->{times};
my $i = 0;
for my $tag ($flv->get_body->get_tags)
{
if ($tag->isa('FLV::VideoTag') || $tag->isa('FLV::AudioTag'))
{
my $time = $tag->get_time;
while ($time >= $times->[$i + 1]->{time})
{
++$i;
}
for my $outfile (@{ $times->[$i]->{out} })
{
my $out = $self->{outfiles}->{$outfile};
my $tag_copy = bless { %{$tag} }, ref $tag; # shallow clone
$tag_copy->{start} -= $out->{cutin};
push @{ $out->{flv}->get_body->{tags} }, $tag_copy;
}
}
}
return;
}
=item $self->save_all()
Serialize all of the extracted FLVs to file.
=cut
sub save_all
{
my $self = shift;
for my $out (values %{ $self->{outfiles} })
{
$out->{flv}->populate_meta();
$out->{flv}->serialize($out->{fh});
}
return;
}
1;
__END__
=back
=head1 AUTHOR
See L<FLV::Info>
=cut