# $Id: Title.pm,v 1.180 2006/08/18 21:45:08 joern Exp $

#-----------------------------------------------------------------------
# Copyright (C) 2001-2006 Jörn Reder <joern AT zyn.de>.
# All Rights Reserved. See file COPYRIGHT for details.
#
# This module is part of Video::DVDRip, which is free software; you can
# redistribute it and/or modify it under the same terms as Perl itself.
#-----------------------------------------------------------------------

package Video::DVDRip::Title;
use Locale::TextDomain qw (video.dvdrip);

use base Video::DVDRip::Base;

use Video::DVDRip::Probe;
use Video::DVDRip::PSU;
use Video::DVDRip::Audio;
use Video::DVDRip::Subtitle;
use Video::DVDRip::BitrateCalc;
use Video::DVDRip::FilterSettings;

use Carp;
use strict;

use FileHandle;
use File::Path;
use File::Basename;
use File::Copy;

# Back reference to the project of this title

sub project			{ shift->{project}			}
sub set_project			{ shift->{project}		= $_[1] }

#------------------------------------------------------------------------
# These attributes are probed from the DVD
#------------------------------------------------------------------------

sub width			{ shift->{width}			}
sub height			{ shift->{height}			}
sub aspect_ratio		{ shift->{aspect_ratio}			}
sub video_mode			{ shift->{video_mode}			}
sub letterboxed			{ shift->{letterboxed}			}
sub frames			{ shift->{frames}			}
sub runtime			{ shift->{runtime}			}
sub frame_rate			{
    my $self = shift;
    my $frame_rate = $self->{frame_rate};
    $frame_rate =~ tr/,/./;
    return $frame_rate;
}
sub bitrates			{ shift->{bitrates}			}
sub audio_tracks		{ shift->{audio_tracks}			}
sub chapters			{ shift->{chapters}			}
sub viewing_angles		{ shift->{viewing_angles}		}
sub dvd_probe_output		{ shift->{dvd_probe_output}		}
sub vob_probe_output		{ shift->{vob_probe_output}		}

sub set_width			{ shift->{width}		= $_[1]	}
sub set_height			{ shift->{height}		= $_[1]	}
sub set_aspect_ratio		{ shift->{aspect_ratio}		= $_[1]	}
sub set_video_mode		{ shift->{video_mode}		= $_[1]	}
sub set_letterboxed		{ shift->{letterboxed}		= $_[1]	}
sub set_frames			{ shift->{frames}		= $_[1]	}
sub set_runtime			{ shift->{runtime}		= $_[1]	}
sub set_frame_rate		{ shift->{frame_rate}		= $_[1]	}
sub set_bitrates		{ shift->{bitrates}		= $_[1]	}
sub set_audio_tracks		{ shift->{audio_tracks}		= $_[1]	}
sub set_chapters		{ shift->{chapters}		= $_[1]	}
sub set_viewing_angles		{ shift->{viewing_angles}	= $_[1]	}
sub set_dvd_probe_output	{ shift->{dvd_probe_output}	= $_[1]	}
sub set_vob_probe_output	{ shift->{vob_probe_output}	= $_[1]	}

#------------------------------------------------------------------------
# Some calculated attributes
#------------------------------------------------------------------------

sub nr				{ shift->{nr}				}
sub size			{ shift->{size}				}
sub audio_channel		{ shift->{audio_channel}		}
sub preset			{ shift->{preset}			}
sub last_applied_preset		{ shift->{last_applied_preset}		}
sub preview_frame_nr		{ shift->{preview_frame_nr}		}
sub files			{ shift->{files}			}
sub actual_chapter		{
	# if no actual chapter is set, this method returns the first
	# chapter, so all functions that are not aware of chapter
	# mode should do something senseful.
	# If you want to have the *real* actual chapter, use the
	# methode ->real_actual_chapter!
	my $self = shift;
	$self->{actual_chapter} ||
	$self->get_first_chapter;
}
sub real_actual_chapter		{ shift->{actual_chapter}		}
sub program_stream_units	{ shift->{program_stream_units}		}
sub bbox_min_x			{ shift->{bbox_min_x}			}
sub bbox_min_y			{ shift->{bbox_min_y}			}
sub bbox_max_x			{ shift->{bbox_max_x}			}
sub bbox_max_y			{ shift->{bbox_max_y}			}
sub chapter_frames		{ shift->{chapter_frames} ||= {}	}

sub set_nr			{ shift->{nr}			= $_[1] }
sub set_size			{ shift->{size}			= $_[1] }
sub set_audio_channel		{ shift->{audio_channel}	= $_[1] }
sub set_preset			{ shift->{preset}		= $_[1] }
sub set_last_applied_preset	{ shift->{last_applied_preset}	= $_[1]	}
sub set_preview_frame_nr	{ shift->{preview_frame_nr}	= $_[1] }
sub set_actual_chapter		{ shift->{actual_chapter}	= $_[1] }
sub set_program_stream_units	{ shift->{program_stream_units}	= $_[1] }
sub set_bbox_min_x		{ shift->{bbox_min_x}		= $_[1]	}
sub set_bbox_min_y		{ shift->{bbox_min_y}		= $_[1]	}
sub set_bbox_max_x		{ shift->{bbox_max_x}		= $_[1]	}
sub set_bbox_max_y		{ shift->{bbox_max_y}		= $_[1]	}
sub set_chapter_frames		{ shift->{chapter_frames}	= $_[1]	}

#------------------------------------------------------------------------
# These attributes must be specified by the user and are
# input parameters for the transcode process.
#------------------------------------------------------------------------

sub tc_container		{ shift->{tc_container}			}
sub tc_viewing_angle		{ shift->{tc_viewing_angle}      	}
sub tc_deinterlace		{ shift->{tc_deinterlace} || 0 		}
sub tc_anti_alias		{ shift->{tc_anti_alias}  || 0 		}
sub tc_clip1_top		{ shift->{tc_clip1_top}			}
sub tc_clip1_bottom		{ shift->{tc_clip1_bottom}		}
sub tc_clip1_left		{ shift->{tc_clip1_left}		}
sub tc_clip1_right		{ shift->{tc_clip1_right}		}
sub tc_zoom_width		{ shift->{tc_zoom_width}		}
sub tc_zoom_height		{ shift->{tc_zoom_height}		}
sub tc_clip2_top		{ shift->{tc_clip2_top}			}
sub tc_clip2_bottom		{ shift->{tc_clip2_bottom}		}
sub tc_clip2_left		{ shift->{tc_clip2_left}		}
sub tc_clip2_right		{ shift->{tc_clip2_right}		}
sub tc_video_codec		{ shift->{tc_video_codec}		}
sub tc_video_af6_codec		{ shift->{tc_video_af6_codec}		}
sub tc_video_bitrate		{ shift->{tc_video_bitrate}      	}
sub tc_video_bitrate_manual	{ shift->{tc_video_bitrate_manual}	}
sub tc_video_bpp		{ shift->{tc_video_bpp}      		}
sub tc_video_bpp_manual		{ shift->{tc_video_bpp_manual}		}
sub tc_video_bitrate_mode	{ shift->{tc_video_bitrate_mode}	}
sub tc_video_bitrate_range	{ shift->{tc_video_bitrate_range}	}
sub tc_video_framerate		{ shift->{tc_video_framerate}      	}
sub tc_fast_bisection		{ shift->{tc_fast_bisection}      	}
sub tc_psu_core			{ shift->{tc_psu_core}      		}
sub tc_keyframe_interval	{ shift->{tc_keyframe_interval}	|| 250	}
sub tc_split			{ shift->{tc_split}			}
sub tc_force_slow_grabbing	{ shift->{tc_force_slow_grabbing}	}

sub tc_target_size		{ shift->{tc_target_size}		}
sub tc_disc_cnt 	    	{ shift->{tc_disc_cnt}			}
sub tc_disc_size	    	{ shift->{tc_disc_size}			}
sub tc_start_frame	    	{ shift->{tc_start_frame}		}
sub tc_end_frame	    	{ shift->{tc_end_frame}			}
sub tc_fast_resize	    	{ shift->{tc_fast_resize}		}
sub tc_multipass	    	{ shift->{tc_multipass}			}
sub tc_multipass_reuse_log	{ shift->{tc_multipass_reuse_log}	}
sub tc_title_nr	    		{ $_[0]->{tc_title_nr} || $_[0]->{nr}	}
sub tc_use_chapter_mode    	{ shift->{tc_use_chapter_mode} || 0	}
sub tc_selected_chapters	{ shift->{tc_selected_chapters}		}
sub tc_options			{ shift->{tc_options}			}
sub tc_nice			{ shift->{tc_nice}			}
sub tc_preview			{ shift->{tc_preview}			}
sub tc_execute_afterwards	{ shift->{tc_execute_afterwards}	}
sub tc_exit_afterwards		{ shift->{tc_exit_afterwards}		}

sub set_tc_viewing_angle	{ shift->{tc_viewing_angle}	= $_[1]	}
sub set_tc_deinterlace		{ shift->{tc_deinterlace}	= $_[1]	}
sub set_tc_anti_alias		{ shift->{tc_anti_alias}	= $_[1]	}
# implemented below : sub set_tc_clip1_top {}
# implemented below : sub set_tc_clip1_bottom {}
# implemented below : sub set_tc_clip1_left {}
# implemented below : sub set_tc_clip1_right {}
# implemented below : sub set_tc_zoom_width {}
# implemented below : sub set_tc_zoom_height {}
# implemented below : sub set_tc_clip2_top {}
# implemented below : sub set_tc_clip2_bottom {}
# implemented below : sub set_tc_clip2_left {}
# implemented below : sub set_tc_clip2_right {}
# implemented below : sub set_tc_video_codec {}
# implemented below : sub set_tc_video_af6_codec {}
sub set_tc_video_bitrate	{ shift->{tc_video_bitrate}  	= $_[1]	}
# implemented below : sub set_tc_video_bitrate_manual {}
sub set_tc_video_bpp		{ shift->{tc_video_bpp}  	= $_[1]	}
# implemented below : sub set_tc_video_bpp_manual {}
# implemented below : sub set_tc_video_bitrate_mode {}
# implemented below : sub set_tc_video_bitrate_range {}
sub set_tc_video_framerate	{ shift->{tc_video_framerate} 	= $_[1]	}
sub set_tc_fast_bisection	{ shift->{tc_fast_bisection} 	= $_[1]	}
sub set_tc_psu_core		{ shift->{tc_psu_core} 		= $_[1]	}
sub set_tc_keyframe_interval	{ shift->{tc_keyframe_interval}	= $_[1]	}
sub set_tc_split		{ shift->{tc_split}		= $_[1]	}
sub set_tc_force_slow_grabbing	{ shift->{tc_force_slow_grabbing}= $_[1]}

# implemented below : sub set_tc_disc_cnt
# implemented below : sub set_tc_disc_size
# implemented below : sub set_tc_target_size
# implemented below : sub set_tc_start_frame
# implemented below : sub set_tc_end_frame
sub set_tc_fast_resize		{ shift->{tc_fast_resize}    	= $_[1]	}
sub set_tc_multipass		{ shift->{tc_multipass}    	= $_[1]	}
sub set_tc_multipass_reuse_log	{ shift->{tc_multipass_reuse_log}= $_[1]}
sub set_tc_title_nr	    	{ shift->{tc_title_nr}    	= $_[1]	}
sub set_tc_use_chapter_mode 	{ shift->{tc_use_chapter_mode}	= $_[1] }
sub set_tc_selected_chapters	{ shift->{tc_selected_chapters}	= $_[1] }
sub set_tc_options		{ shift->{tc_options}		= $_[1] }
sub set_tc_nice			{ shift->{tc_nice}		= $_[1] }
sub set_tc_preview		{ shift->{tc_preview}		= $_[1] }
sub set_tc_execute_afterwards	{ shift->{tc_execute_afterwards}= $_[1]	}
sub set_tc_exit_afterwards	{ shift->{tc_exit_afterwards}	= $_[1]	}

#-- Attributes for storage ----------------------------------------------

sub storage_video_size      	{ shift->{storage_video_size}	    	}
sub storage_audio_size      	{ shift->{storage_audio_size}	    	}
sub storage_other_size      	{ shift->{storage_other_size}	    	}
sub storage_total_size      	{ shift->{storage_total_size}	    	}

sub set_storage_video_size	{ shift->{storage_video_size}	= $_[1]	}
sub set_storage_audio_size	{ shift->{storage_audio_size}	= $_[1]	}
sub set_storage_other_size	{ shift->{storage_other_size}	= $_[1]	}
sub set_storage_total_size	{ shift->{storage_total_size}	= $_[1]	}

sub bitrate_calc		{ shift->{bitrate_calc}			}
sub set_bitrate_calc		{ shift->{bitrate_calc}		= $_[1]	}

#-- Attributes for CD burning -------------------------------------------

sub burn_cd_type		{ shift->{burn_cd_type} || 'iso'	}
sub burn_label			{ shift->{burn_label}			}
sub burn_abstract		{ shift->{burn_abstract}		}
sub burn_number			{ shift->{burn_number}			}
sub burn_abstract_sticky	{ shift->{burn_abstract_sticky}		}
sub burn_files_selected		{ shift->{burn_files_selected}		}

sub set_burn_cd_type		{ shift->{burn_cd_type}		= $_[1]	}
sub set_burn_label		{ shift->{burn_label}		= $_[1]	}
sub set_burn_abstract		{ shift->{burn_abstract}	= $_[1]	}
sub set_burn_number		{ shift->{burn_number}		= $_[1]	}
sub set_burn_abstract_sticky	{ shift->{burn_abstract_sticky}	= $_[1]	}
sub set_burn_files_selected	{ shift->{burn_files_selected}	= $_[1]	}

#-- Attributes for subtitles --------------------------------------------

sub subtitles			{ shift->{subtitles}			}
sub selected_subtitle_id	{ shift->{selected_subtitle_id}		}
sub subtitle_test		{ shift->{subtitle_test}		}
sub tc_rip_subtitle_mode	{ shift->{tc_rip_subtitle_mode}		}
sub tc_rip_subtitle_lang	{ shift->{tc_rip_subtitle_lang}		}

sub set_subtitles		{ shift->{subtitles}		= $_[1]	}
sub set_selected_subtitle_id	{ shift->{selected_subtitle_id}	= $_[1]	}
sub set_subtitle_test		{ shift->{subtitle_test}	= $_[1]	}
sub set_tc_rip_subtitle_mode	{ shift->{tc_rip_subtitle_mode}	= $_[1]	}
sub set_tc_rip_subtitle_lang    { shift->{tc_rip_subtitle_lang}	= $_[1]	}

#-- Filter Settings -----------------------------------------------------

sub tc_filter_settings {
    my $self = shift;
    if ( not $self->{tc_filter_settings} ) {
        return $self->{tc_filter_settings}
            = Video::DVDRip::FilterSettings->new;
    }
    return $self->{tc_filter_settings};
}

sub tc_filter_setting_id        { shift->{tc_filter_setting_id}         }
sub set_tc_filter_setting_id    { shift->{tc_filter_setting_id} = $_[1] }

sub tc_selected_filter_setting {
    my $self = shift;
    return if not $self->tc_filter_setting_id;
    return $self->tc_filter_settings->get_filter_instance(
        id => $self->tc_filter_setting_id );
}

sub tc_preview_start_frame      { shift->{tc_preview_start_frame}       }
sub tc_preview_end_frame        { shift->{tc_preview_end_frame}         }
sub tc_preview_buffer_frames    { shift->{tc_preview_buffer_frames}||20 }

sub set_tc_preview_start_frame  { shift->{tc_preview_start_frame}   = $_[1] }
sub set_tc_preview_end_frame    { shift->{tc_preview_end_frame}     = $_[1] }
sub set_tc_preview_buffer_frames{ shift->{tc_preview_buffer_frames} = $_[1] }

sub tc_use_yuv_internal {
    my $self = shift;

    # enabled only if all selected filters support YUV
    # and we have no odd clipping / resizing

    return 0
        if $self->tc_clip1_left % 2
        or $self->tc_clip1_right % 2
        or $self->tc_clip1_top % 2
        or $self->tc_clip1_bottom % 2
        or $self->tc_clip2_left % 2
        or $self->tc_clip2_right % 2
        or $self->tc_clip2_top % 2
        or $self->tc_clip2_bottom % 2
        or $self->tc_zoom_width % 2
        or $self->tc_zoom_height % 2;

    foreach my $filter_instance ( @{ $self->tc_filter_settings->filters } ) {
        return 0
            if $filter_instance->get_filter->can_video
            and not $filter_instance->get_filter->can_yuv;
    }

    return 1;
}

#-- Constructor ---------------------------------------------------------

sub new {
    my $class = shift;
    my %par   = @_;
    my ( $nr, $project ) = @par{ 'nr', 'project' };

    my $default_subtitle_grab = $class->config('default_subtitle_grab');

    my $self = {
        project              => $project,
        nr                   => $nr,
        size                 => 0,
        files                => [],
        audio_channel        => 0,
        scan_result          => undef,
        tc_clip1_top         => 0,
        tc_clip1_bottom      => 0,
        tc_clip1_left        => 0,
        tc_clip1_right       => 0,
        tc_clip2_top         => 0,
        tc_clip2_bottom      => 0,
        tc_clip2_left        => 0,
        tc_clip2_right       => 0,
        tc_rip_subtitle_mode => $default_subtitle_grab,
        tc_selected_chapters => [],
        tc_preview_buffer_frames => 20,
        program_stream_units => [],
        chapter_frames       => {},
        tc_filter_settings   => Video::DVDRip::FilterSettings->new,
    };

    return bless $self, $class;
}

sub set_tc_video_codec {
    my $self = shift;
    my ($value) = @_;

    $self->{tc_video_codec} = $value;

    $self->set_tc_video_af6_codec('mpeg4') if $value eq 'ffmpeg';
    $self->set_tc_video_af6_codec('') if $value ne 'ffmpeg';

    if ( $value eq 'VCD' ) {
        $self->audio_track->set_tc_bitrate(224);
        $self->audio_track->set_tc_audio_codec('mp2');
        $self->set_tc_multipass(0);
        $self->set_tc_video_bitrate_manual(1152);
        $self->set_tc_video_bitrate_mode("manual");

    }
    elsif ( $value =~ /^(X?S?VCD|CVD)$/ ) {
        $self->audio_track->set_tc_audio_codec('mp2');
        $self->set_tc_multipass(0);
    }

    if ( $value =~ /^X?S?VCD$/ ) {
        foreach my $audio ( @{ $self->audio_tracks } ) {
            $audio->set_tc_mp2_samplerate(44100);
        }
    }

    return $value;
}

sub set_tc_video_af6_codec {
    my $self = shift;
    my ($value) = @_;

    $self->{tc_video_af6_codec} = $value;

    $self->set_tc_multipass(0) if $value eq 'h264';

    return $value;
}

#-- get actually selected audio (or a dummy object, if no track is selected)

sub audio_track {
    my $self = shift;
    if ( $self->audio_channel == -1 ) {
        return;

        # no audio track selected. create a dummy object.
        # (probably this title has no audio at all)
        return Video::DVDRip::Audio->new( title => $self );
    }
    return $self->audio_tracks->[ $self->audio_channel ];
}

sub set_tc_container {
    my $self = shift;
    my ($container) = @_;

    return $container if $container eq $self->tc_container;

    $self->{tc_container} = $container;

    return if not defined $self->audio_tracks;

    my @messages;

    if ( $container eq 'avi' ) {

        # no vorbis and mp2 audio here
        foreach my $audio ( @{ $self->audio_tracks } ) {
            next if $audio->tc_target_track == -1;
            if ( $audio->tc_audio_codec eq 'vorbis' ) {
                push @messages,
                    __x(
                    "Set codec of audio track #{nr} to 'mp3', "
                        . "'vorbis' not supported by AVI container",
                    nr => $audio->tc_nr
                    );
                $audio->set_tc_audio_codec('mp3');
            }
            elsif ( $audio->tc_audio_codec eq 'mp2' ) {
                push @messages,
                    __x(
                    "Set codec of audio track #{nr} to 'mp3', "
                        . "'mp2' not supported by AVI container",
                    nr => $audio->tc_nr
                    );
                $audio->set_tc_audio_codec('mp3');
            }
        }

        # no (S)VCD here
        if ( $self->tc_video_codec =~ /^(X?S?VCD|CVD)$/ ) {
            push @messages,
                __ "Set video codec to 'xvid', '"
                . $self->tc_video_codec
                . __ "' not supported by AVI container";
            $self->set_tc_video_codec("xvid");
        }

    }
    elsif ( $container eq 'vcd' ) {

        # only mp2 audio here
        foreach my $audio ( @{ $self->audio_tracks } ) {
            next if $audio->tc_target_track == -1;
            if ( $audio->tc_audio_codec ne 'mp2' ) {
                push @messages,
                    __x( "Set codec of audio track #{nr} to 'mp2', '",
                    nr => $audio->tc_nr )
                    . $audio->tc_audio_codec
                    . __ "' not supported by MPEG container";
                $audio->set_tc_audio_codec('mp2');
            }
        }

        # only (S)VCD here
        if ( $self->tc_video_codec !~ /^(X?S?VCD|CVD)$/ ) {
            push @messages,
                __ "Set video codec to 'SVCD', '"
                . $self->tc_video_codec
                . __ "' not supported by MPEG container";
            $self->set_tc_video_codec("SVCD");
        }

    }
    elsif ( $container eq 'ogg' ) {

        # no mp2 and pcm audio here
        foreach my $audio ( @{ $self->audio_tracks } ) {
            next if $audio->tc_target_track == -1;
            if (   $audio->tc_audio_codec eq 'mp2'
                or $audio->tc_audio_codec eq 'pcm' ) {
                push @messages,
                    __x( "Set codec of audio track #{nr} to 'vorbis', '",
                    nr => $audio->tc_nr )
                    . $audio->tc_audio_codec
                    . __ "' not supported by OGG container";
                $audio->set_tc_audio_codec('vorbis');
            }
        }

        # no (S)VCD here
        if ( $self->tc_video_codec =~ /^(X?S?VCD|CVD)$/ ) {
            $self->set_tc_video_codec("xvid");
            push @messages, __
                "Set video codec to 'xvid', MPEG not supported by OGG container";
        }
    }

    foreach my $msg (@messages) {
        $self->log($msg);
    }

    $self->calc_video_bitrate;

    return $container;
}

sub set_tc_disc_cnt {
    my $self = shift;
    my ($cnt) = @_;
    $self->{tc_disc_cnt} = $cnt;
    $self->set_tc_target_size( $cnt * $self->tc_disc_size );
    return $cnt;
}

sub set_tc_disc_size {
    my $self = shift;
    my ($size) = @_;
    $self->{tc_disc_size} = $size;
    $self->set_tc_target_size( $self->tc_disc_cnt * $size );
    return $size;
}

sub set_tc_target_size {
    my $self = shift;
    my ($value) = @_;
    $self->{tc_target_size} = $value;
    $self->calc_video_bitrate;
    return $value;
}

sub set_tc_video_bitrate_manual {
    my $self = shift;
    my ($size) = @_;
    $self->{tc_video_bitrate_manual} = $size;
    $self->calc_video_bitrate;
    return $size;
}

sub set_tc_video_bpp_manual {
    my $self = shift;
    my ($value) = @_;
    $self->{tc_video_bpp_manual} = $value;
    $self->calc_video_bitrate;
    return $value;
}

sub set_tc_video_bitrate_mode {
    my $self = shift;
    my ($value) = @_;
    $self->{tc_video_bitrate_mode} = $value;
    $self->calc_video_bitrate;
    return $value;
}

sub set_tc_video_bitrate_range {
    my $self = shift;
    my ($value) = @_;
    $self->{tc_video_bitrate_range} = $value;
    $self->calc_video_bitrate;
    return $value;
}

sub set_tc_start_frame {
    my $self = shift;
    my ($value) = @_;
    $self->{tc_start_frame} = $value;
    $self->calc_video_bitrate;
    return $value;
}

sub set_tc_end_frame {
    my $self = shift;
    my ($value) = @_;
    $self->{tc_end_frame} = $value;
    $self->calc_video_bitrate;
    return $value;
}

#---------------------

sub set_tc_clip1_top {
    my $self = shift;
    my ($value) = @_;
    $self->{tc_clip1_top} = $value;
    $self->calc_video_bitrate;
    return $value;
}

sub set_tc_clip1_bottom {
    my $self = shift;
    my ($value) = @_;
    $self->{tc_clip1_bottom} = $value;
    $self->calc_video_bitrate;
    return $value;
}

sub set_tc_clip1_left {
    my $self = shift;
    my ($value) = @_;
    $self->{tc_clip1_left} = $value;
    $self->calc_video_bitrate;
    return $value;
}

sub set_tc_clip1_right {
    my $self = shift;
    my ($value) = @_;
    $self->{tc_clip1_right} = $value;
    $self->calc_video_bitrate;
    return $value;
}

sub set_tc_zoom_width {
    my $self = shift;
    my ($value) = @_;
    $self->{tc_zoom_width} = $value;
    $self->calc_video_bitrate;
    return $value;
}

sub set_tc_zoom_height {
    my $self = shift;
    my ($value) = @_;
    $self->{tc_zoom_height} = $value;
    $self->calc_video_bitrate;
    return $value;
}

sub set_tc_clip2_top {
    my $self = shift;
    my ($value) = @_;
    $self->{tc_clip2_top} = $value;
    $self->calc_video_bitrate;
    return $value;
}

sub set_tc_clip2_bottom {
    my $self = shift;
    my ($value) = @_;
    $self->{tc_clip2_bottom} = $value;
    $self->calc_video_bitrate;
    return $value;
}

sub set_tc_clip2_left {
    my $self = shift;
    my ($value) = @_;
    $self->{tc_clip2_left} = $value;
    $self->calc_video_bitrate;
    return $value;
}

sub set_tc_clip2_right {
    my $self = shift;
    my ($value) = @_;
    $self->{tc_clip2_right} = $value;
    $self->calc_video_bitrate;
    return $value;
}

sub is_ogg {
    my $self = shift;
    return $self->tc_container eq 'ogg';
}

sub is_mpeg {
    my $self = shift;
    return $self->tc_container eq 'vcd';
}

sub has_vbr_audio {
    my $self = shift;

    return 0 if $self->tc_video_bitrate_mode eq 'manual';

    foreach my $audio ( @{ $self->audio_tracks } ) {
        next if $audio->tc_target_track == -1;
        return 1 if $audio->tc_audio_codec eq 'vorbis';
    }

    return 0;
}

sub vob_dir {
    my $self = shift;

    my $vob_dir;

    if ( $self->tc_use_chapter_mode ) {
        $vob_dir = sprintf( "%s/%03d-C%03d/",
            $self->project->vob_dir, $self->nr,
            ( $self->actual_chapter || $self->get_first_chapter || 1 ) );

    }
    else {
        $vob_dir = sprintf( "%s/%03d/", $self->project->vob_dir, $self->nr );
    }

    return $vob_dir;
}

sub get_vob_size {
    my $self = shift;

    return 1 if $self->project->rip_mode ne 'rip';

    my $vob_dir = $self->vob_dir;

    my $vob_size = 0;
    $vob_size += -s for <$vob_dir/*>;
    $vob_size = int( $vob_size / 1024 / 1024 );

    return $vob_size;
}

sub get_title_info {
    my $self = shift;

    my $fps = $self->frame_rate;
    $fps =~ s/\.0+$//;

    my $length = $self->runtime - 1;
    my $h      = int( $length / 3600 );
    my $m      = int( ( $length - $h * 3600 ) / 60 );
    my $s      = $length - $h * 3600 - $m * 60;

    $length = sprintf( "%02d:%02d:%02d", $h, $m, $s );

    return $length . ", "
        . uc( $self->video_mode ) . ", "
        . $self->chapters . " "
        . __("Chp") . ", "
        . scalar( @{ $self->audio_tracks } ) . " "
        . __("Aud") . ", "
        . "$fps fps, "
        . $self->aspect_ratio . ", "
        . $self->frames . " "
        . __("frames") . ", "
        . $self->width . "x"
        . $self->height

}

sub transcode_data_source {
    my $self = shift;

    my $project = $self->project;
    my $mode    = $project->rip_mode;

    my $source;

    if ( $mode eq 'rip' ) {
        $source = $self->vob_dir;

    }
    else {
        $source = $project->rip_data_source;

    }

    return $source;
}

sub data_source_options {
    my $self = shift;
    my %par = @_;
    my ($audio_channel) = @par{'audio_channel'};

    $audio_channel = $self->audio_channel
        if not defined $audio_channel;

    my $mode   = $self->project->rip_mode;
    my $source = $self->transcode_data_source;

    my ( $input_filter, $need_title );

    if ( $mode eq 'rip' ) {
        $input_filter = "vob";
        $need_title   = 0;

    }
    else {
        $input_filter = "dvd";
        $need_title   = 1;

    }

    $input_filter .= ",null" if $audio_channel == -1;

    my %options = (
        i => $source,
        x => $input_filter
    );

    if ($need_title) {
        my $chapter = $self->actual_chapter || -1;
        $options{T} = $self->nr . ",$chapter," . $self->tc_viewing_angle;
    }

    return \%options;
}

sub create_vob_dir {
    my $self = shift;

    my $vob_dir = $self->vob_dir;

    if ( not -d $vob_dir ) {
        mkpath( [$vob_dir], 0, 0755 )
            or croak __x( "Can't mkpath directory '{dir}'", dir => $vob_dir );
    }

    1;
}

sub avi_dir {
    my $self = shift;

    return sprintf( "%s/%03d", $self->project->avi_dir, $self->nr, );
}

sub get_target_ext {
    my $self = shift;

    my $video_codec = $self->tc_video_codec;
    my $ext         = ( $video_codec =~ /^(X?S?VCD|CVD)$/ ) ? "" : ".avi";

    $ext = "." . $self->config('ogg_file_ext') if $self->is_ogg;

    return $ext;
}

sub avi_file {
    my $self = shift;

    my $ext = $self->get_target_ext;

    my $target_dir =
          $self->subtitle_test
        ? $self->get_subtitle_preview_dir
        : $self->avi_dir;

    if ( $self->tc_use_chapter_mode ) {
        return sprintf(
            "%s/%s-%03d-C%03d$ext",
            $target_dir, $self->project->name,
            $self->nr,   $self->actual_chapter
        );
    }
    else {
        return sprintf( "%s/%s-%03d$ext",
            $target_dir, $self->project->name, $self->nr );
    }
}

sub target_avi_file {
    my $self = shift;
    return $self->avi_file;
}

sub target_avi_audio_file {
    my $self = shift;
    my %par  = @_;
    my ( $vob_nr, $avi_nr ) = @par{ 'vob_nr', 'avi_nr' };

    my $ext = $self->is_ogg ? "." . $self->config('ogg_file_ext') : '.avi';
    $ext = "" if $self->tc_container eq 'vcd';

    my $audio_file = $self->target_avi_file;
    $audio_file =~ s/\.[^.]+$//;
    $audio_file = sprintf( "%s-%02d$ext", $audio_file, $avi_nr );

    return $audio_file;
}

sub multipass_log_dir {
    my $self = shift;
    return dirname( $self->preview_filename );
}

sub create_avi_dir {
    my $self = shift;

    my $avi_dir = dirname $self->avi_file;

    if ( not -d $avi_dir ) {
        mkpath( [$avi_dir], 0, 0755 )
            or croak __x( "Can't mkpath directory '{dir}'", dir => $avi_dir );
    }

    1;
}

sub preview_filename {
    my $self = shift;
    my %par = @_;
    my ($type) = @par{'type'};

    return sprintf( "%s/%s-%03d-preview-%s.jpg",
        $self->project->snap_dir, $self->project->name, $self->nr, $type );
}

sub preview_filename_orig {
    shift->preview_filename( type => "orig" );
}

sub preview_filename_clip1 {
    shift->preview_filename( type => "clip1" );
}

sub preview_filename_zoom {
    shift->preview_filename( type => "zoom" );
}

sub preview_filename_clip2 {
    shift->preview_filename( type => "clip2" );
}

sub preview_scratch_filename {
    my $self = shift;
    my %par = @_;
    my ($type) = @par{'type'};

    return sprintf( "%s/%s-%03d-preview-scratch-%s.jpg",
        $self->project->snap_dir, $self->project->name, $self->nr, $type );
}

sub preview_label {
    my $self = shift;
    my %par = @_;
    my ($type, $details) = @par{'type','details'};

    my ( $width, $height, $warn_width, $warn_height, $text, $ratio,
        $phys_ratio );
    ( $width, $height, $ratio ) = $self->get_effective_ratio( type => $type );

    $ratio = "4:3"  if $ratio >= 1.32 and $ratio <= 1.34;
    $ratio = "16:9" if $ratio >= 1.76 and $ratio <= 1.78;

    ($ratio) = $ratio =~ /(\d+[.,]\d{1,2})/ if $ratio !~ /:/;

    $phys_ratio = $width / $height;
    ($phys_ratio) = $phys_ratio =~ /(\d+[.,]\d{1,2})/;

    $warn_width  = ( $type eq 'clip2' and $width % 16 )  ? "!16" : "";
    $warn_height = ( $type eq 'clip2' and $height % 16 ) ? "!16" : "";

    $warn_width  ||= ( $width % 2 )  ? "!2" : "";
    $warn_height ||= ( $height % 2 ) ? "!2" : "";

    if ( $type eq 'clip1' ) {
        $warn_height ||= "!"
            if $self->tc_clip1_top % 2
            or $self->tc_clip1_bottom % 2;
        $warn_width ||= "!"
            if $self->tc_clip1_left % 2
            or $self->tc_clip1_right % 2;
    }

    if ( $type eq 'clip2' ) {
        $warn_height ||= "!"
            if $self->tc_clip2_top % 2
            or $self->tc_clip2_bottom % 2;
        $warn_width ||= "!"
            if $self->tc_clip2_left % 2
            or $self->tc_clip2_right % 2;
    }

    if ( $details ) {
        my @status;
        if ( $warn_width =~ /2/ ) {
            push @status, __"Width isn't even.";
        }
        elsif ( $warn_width =~ /16/ ) {
            push @status, __"Width is not divisible by 16.";
        }
        if ( $warn_height =~ /2/ ) {
            push @status, __"Height isn't even.";
        }
        elsif ( $warn_height =~ /16/ ) {
            push @status, __"Height is not divisible by 16.";
        }
        $text = join (" ", @status);
        if ( $text ) {
            $text = qq[<span foreground="red"><b>$text</b></span>];
        }
        else {
            $text = qq[<span foreground="#007700"><b>].
                    __("Settings Ok").
                    qq[</b></span>];
        }
    }
    else {
        my $type_text =
              $type eq 'clip1' ? __ "After 1st clipping"
            : $type eq 'clip2' ? __ "After 2nd clipping"
            : __ "After zoom";

        $text = sprintf(
            "<u>$type_text</u>: <b>%d%sx%d%s</b>\n"
                . __x(
                "Eff. ratio: <b>{eff}</b>, phys. ratio: <b>{phys}</b>",
                eff  => $ratio,
                phys => $phys_ratio
                ),
            $width,
            qq[<span foreground="red">$warn_width</span>],
            $height,
            qq[<span foreground="red">$warn_height</span>],
        );
    }

    return $text;
}

sub preview_label_clip1 {
    shift->preview_label( type => "clip1" );
}

sub preview_label_zoom {
    shift->preview_label( type => "zoom" );
}

sub preview_label_clip2 {
    shift->preview_label( type => "clip2" );
}

sub vob_nav_file {
    my $self = shift;

    my $file;
    if ( $self->tc_use_chapter_mode ) {
        $file = sprintf( "%s/%s-%03d-C%03d-nav.log",
            $self->project->snap_dir, $self->project->name, $self->nr,
            $self->actual_chapter );
    }
    else {
        $file = sprintf( "%s/%s-%03d-nav.log",
            $self->project->snap_dir, $self->project->name, $self->nr );
    }

    return $file;
}

sub has_vob_nav_file {
    my $self = shift;

    my $old_chapter = $self->actual_chapter;

    $self->set_actual_chapter( $self->get_first_chapter )
        if $self->tc_use_chapter_mode;

    my $vob_nav_file = $self->vob_nav_file;

    $self->set_actual_chapter($old_chapter)
        if $self->tc_use_chapter_mode;

    return -f $vob_nav_file;
}

sub audio_wav_file {
    my $self = shift;

    my $chap;
    if ( $self->actual_chapter ) {
        $chap = sprintf( "-C%02d", $self->actual_chapter );
    }

    return sprintf(
        "%s/%s-%03d-%02d$chap.wav",
        $self->avi_dir, $self->project->name,
        $self->nr,      $self->audio_track->tc_nr,
    );
}

sub add_vob {
    my $self = shift;
    my %par = @_;
    my ($file) = @par{'file'};

    $self->set_size( $self->size + ( -s $file ) );
    push @{ $self->files }, $file;

    1;
}

sub apply_preset {
    my $self     = shift;
    my %par      = @_;
    my ($preset) = @par{'preset'};

    $preset ||= $self->config_object->get_preset( name => $self->preset );

    return 1 if not $preset;

    $self->set_last_applied_preset( $preset->name );

    if ( $preset->auto ) {
        $self->auto_adjust_clip_zoom(
            frame_size  => $preset->frame_size,
            fast_resize => $preset->tc_fast_resize,
        );
        return 1;
    }

    my $attributes = $preset->attributes;
    my $set_method;
    foreach my $attr ( @{$attributes} ) {
        $set_method = "set_$attr";
        $self->$set_method( $preset->$attr() );
    }

    1;
}

sub get_chapters {
    my $self = shift;

    my @chapters;
    if ( $self->tc_use_chapter_mode eq 'select' ) {
        @chapters = sort { $a <=> $b } @{ $self->tc_selected_chapters || [] };
    }
    else {
        @chapters = ( 1 .. $self->chapters );
    }

    return \@chapters;
}

sub get_first_chapter {
    my $self = shift;

    my $chapter_mode = $self->tc_use_chapter_mode;
    return if not $chapter_mode;

    if ( $chapter_mode eq 'select' ) {
        my $chapters = $self->get_chapters;
        return $chapters->[0];
    }
    else {
        return 1;
    }
}

sub get_last_chapter {
    my $self = shift;

    my $chapter_mode = $self->tc_use_chapter_mode;
    return if not $chapter_mode;

    my $chapters = $self->get_chapters;
    return $chapters->[ @{$chapters} - 1 ];
}

sub calc_program_stream_units {
    my $self = shift;

    my $vob_nav_file = $self->vob_nav_file;

    my $fh = FileHandle->new;
    open( $fh, $vob_nav_file )
        or croak __x( "Can't read VOB navigation file '{filename}'",
        filename => $vob_nav_file );

    my $current_unit = 0;
    my ( @program_stream_units, $unit, $frame, $last_frame );

    while (<$fh>) {
        ( $unit, $frame ) = /(\d+)\s+(\d+)/;
        if ( $unit != $current_unit ) {
            push @program_stream_units,
                Video::DVDRip::PSU->new(
                nr     => $current_unit,
                frames => $last_frame,
                );
            $current_unit = $unit;
        }
        $last_frame = $frame;
    }

    if ( $last_frame != 0 ) {
        push @program_stream_units,
            Video::DVDRip::PSU->new(
            nr     => $current_unit,
            frames => $last_frame,
            );
    }

    close $fh;

    $self->set_program_stream_units( \@program_stream_units );

    $self->log( __ "Program stream units calculated" );

    1;
}

sub get_effective_ratio {
    my $self   = shift;
    my %par    = @_;
    my ($type) = @par{'type'};    # clip1, zoom, clip2

    my $width       = $self->width;
    my $height      = $self->height || 1;
    my $clip1_ratio = $width / $height;

    my $from_width  = $width - $self->tc_clip1_left - $self->tc_clip1_right;
    my $from_height = $height - $self->tc_clip1_top - $self->tc_clip1_bottom;

    return ( $from_width, $from_height, $clip1_ratio ) if $type eq 'clip1';

    my $zoom_width  = $self->tc_zoom_width  || $from_width;
    my $zoom_height = $self->tc_zoom_height || $from_height;
    my $zoom_ratio = ( $zoom_width / $zoom_height ) * ( $width / $height )
        / ( $from_width / $from_height );

    return ( $zoom_width, $zoom_height, $zoom_ratio ) if $type eq 'zoom';

    my $clip2_width
        = $zoom_width - $self->tc_clip2_left - $self->tc_clip2_right;
    my $clip2_height
        = $zoom_height - $self->tc_clip2_top - $self->tc_clip2_bottom;

    return ( $clip2_width, $clip2_height, $zoom_ratio );
}

sub auto_adjust_clip_zoom {
    my $self = shift;
    my %par  = @_;
    my ( $frame_size, $fast_resize ) = @par{ 'frame_size', 'fast_resize' };

    croak __x( "invalid parameter for frame_size ('{frame_size}')",
        frame_size => $frame_size )
        if not $frame_size =~ /^(big|medium|small)$/;

    my %width_presets;
    if ($fast_resize) {
        %width_presets = (
            small  => 496,
            medium => 640,
            big    => 720,
        );
    }
    else {
        %width_presets = (
            small  => 496,
            medium => 640,
            big    => 768,
        );
    }

    $self->set_tc_fast_resize($fast_resize);

    my $results = $self->calculator;

    my $target_width = $width_presets{$frame_size};

    my %result_by_ar_err;
    my $range = 16;
    while ( keys(%result_by_ar_err) == 0 and $range < 1024 ) {
        foreach my $result ( @{$results} ) {
            next if abs( $target_width - $result->{clip2_width} ) > $range;
            $result_by_ar_err{ abs( $result->{ar_err} ) }
                ->{ abs( $target_width - $result->{clip2_width} ) } = $result;
        }
        $range += 16;
    }

    my ($min_err) = sort { $a <=> $b } keys %result_by_ar_err;
    my ($min_width_diff)
        = sort { $a <=> $b } keys %{ $result_by_ar_err{$min_err} };
    my $result = $result_by_ar_err{$min_err}->{$min_width_diff};

    $self->set_tc_zoom_width( $result->{zoom_width} );
    $self->set_tc_zoom_height( $result->{zoom_height} );
    $self->set_tc_clip1_left( $result->{clip1_left} );
    $self->set_tc_clip1_right( $result->{clip1_right} );
    $self->set_tc_clip1_top( $result->{clip1_top} );
    $self->set_tc_clip1_bottom( $result->{clip1_bottom} );
    $self->set_tc_clip2_left( $result->{clip2_left} );
    $self->set_tc_clip2_right( $result->{clip2_right} );
    $self->set_tc_clip2_top( $result->{clip2_top} );
    $self->set_tc_clip2_bottom( $result->{clip2_bottom} );

    1;
}

sub calc_zoom {
    my $self = shift;
    my %par  = @_;
    my ( $width, $height ) = @par{ 'width', 'height' };

    my $result = $self->get_zoom_parameters(
        target_width  => ( $height ? $self->tc_zoom_width  : undef ),
        target_height => ( $width  ? $self->tc_zoom_height : undef ),
        fast_resize_align => ( $self->tc_fast_resize ? 8 : 0 ),
        result_align => 16,
        result_align_clip2 => 1,
        auto_clip          => 0,
        use_clip1          => 1,
    );

    $self->set_tc_zoom_width( $result->{zoom_width} );
    $self->set_tc_zoom_height( $result->{zoom_height} );
    $self->set_tc_clip1_left( $result->{clip1_left} );
    $self->set_tc_clip1_right( $result->{clip1_right} );
    $self->set_tc_clip1_top( $result->{clip1_top} );
    $self->set_tc_clip1_bottom( $result->{clip1_bottom} );
    $self->set_tc_clip2_left( $result->{clip2_left} );
    $self->set_tc_clip2_right( $result->{clip2_right} );
    $self->set_tc_clip2_top( $result->{clip2_top} );
    $self->set_tc_clip2_bottom( $result->{clip2_bottom} );

    1;
}

sub calculator {
    my $self = shift;
    my %par  = @_;
    my ( $fast_resize_align, $result_align, $result_align_clip2 )
        = @par{ 'fast_resize_align', 'result_align', 'result_align_clip2' };
    my ( $auto_clip, $use_clip1, $video_bitrate )
        = @par{ 'auto_clip', 'use_clip1', 'video_bitrate' };

    $fast_resize_align = $self->tc_fast_resize * 8
        if not defined $fast_resize_align;
    $result_align       = 16 if not defined $result_align;
    $result_align_clip2 = 1  if not defined $result_align_clip2;
    $auto_clip          = 1  if not defined $auto_clip;
    $use_clip1          = 0  if not defined $use_clip1;

    my ( $width, $height ) = ( $self->width, $self->height );

    my @result;
    my $last_result;
    my ( $actual_width, $actual_height, $best_result );

    for ( my $i = 0;; ++$i ) {
        my $result = $self->get_zoom_parameters(
            step               => $i,
            step_size          => 1,
            auto_clip          => $auto_clip,
            use_clip1          => $use_clip1,
            fast_resize_align  => $fast_resize_align,
            result_align       => $result_align,
            result_align_clip2 => $result_align_clip2,
            video_bitrate      => $video_bitrate,
        );

        last if $result->{clip2_width} < 200;
        next if $result->{ar_err} > 1;
        next
            if $fast_resize_align
            and ( ( $result->{clip1_width} > $result->{zoom_width} )
            xor( $result->{clip1_height} > $result->{zoom_height} ) );

        if ($i != 0
            and (  $actual_width != $result->{clip2_width}
                or $actual_height != $result->{clip2_height} )
            ) {
            push @result, $best_result;
            $best_result = undef;
        }

        if ( not $best_result
            or $best_result->{ar_err} > $result->{ar_err} ) {
            $best_result = $result;
        }

        $actual_width  = $result->{clip2_width};
        $actual_height = $result->{clip2_height};
    }

    push @result, $best_result if $best_result;

    return \@result;
}

sub get_zoom_parameters {
    my $self = shift;
    my %par = @_;
    my  ($target_width, $target_height, $fast_resize_align) =
    @par{'target_width','target_height','fast_resize_align'};
    my  ($result_align, $result_align_clip2, $auto_clip, $step) =
    @par{'result_align','result_align_clip2','auto_clip','step'};
    my  ($step_size, $use_clip1, $video_bitrate) =
    @par{'step_size','use_clip1','video_bitrate'};

    #use Data::Dumper; print Dumper(\%par);

    my ( $clip1_top, $clip1_bottom, $clip1_left, $clip1_right );
    my ( $clip_top,  $clip_bottom,  $clip_left,  $clip_right );

    my ( $width, $height ) = ( $self->width, $self->height );
    $height ||= 1;
    my $ar = $self->aspect_ratio eq '16:9' ? 16 / 9 : 4 / 3;
    my $ar_width_factor = $ar / ( $width / $height );
    my $zoom_align = $fast_resize_align ? $fast_resize_align : 2;
    $zoom_align ||= $result_align if not $result_align_clip2;
    $use_clip1 = 1 if not $auto_clip;
    $video_bitrate ||= $self->tc_video_bitrate;

    #print "width=$width height=$height\n";

    # clip image
    if ($auto_clip) {
        $clip_top = $self->bbox_min_y || 0;
        $clip_bottom
            = defined $self->bbox_max_y ? $height - $self->bbox_max_y : 0;
        $clip_left = $self->bbox_min_x || 0;
        $clip_right
            = defined $self->bbox_max_x ? $width - $self->bbox_max_x : 0;
    }
    else {
        $clip_top    = $self->tc_clip1_top;
        $clip_bottom = $self->tc_clip1_bottom;
        $clip_left   = $self->tc_clip1_left;
        $clip_right  = $self->tc_clip1_right;
    }

    if ($use_clip1) {
        $clip1_top    = $clip_top;
        $clip1_bottom = $clip_bottom;
        $clip1_left   = $clip_left;
        $clip1_right  = $clip_right;
    }
    else {
        $clip1_top    = 0;
        $clip1_bottom = 0;
        $clip1_left   = 0;
        $clip1_right  = 0;
    }

    # align clip1 values when fast resizing is enabled
    if ($fast_resize_align) {
        $clip1_left   = int( $clip1_left / $zoom_align ) * $zoom_align;
        $clip1_right  = int( $clip1_right / $zoom_align ) * $zoom_align;
        $clip1_top    = int( $clip1_top / $zoom_align ) * $zoom_align;
        $clip1_bottom = int( $clip1_bottom / $zoom_align ) * $zoom_align;
    }

    # no odd clip values
    --$clip1_left   if $clip1_left % 2;
    --$clip1_right  if $clip1_right % 2;
    --$clip1_top    if $clip1_top % 2;
    --$clip1_bottom if $clip1_bottom % 2;

    # calculate start width and height
    my $clip_width  = $width - $clip1_left - $clip1_right;
    my $clip_height = $height - $clip1_top - $clip1_bottom;

    #print "clip_width=$clip_width clip_height=$clip_height\n";

    if ( not $target_height ) {
        $target_width
            ||= int( $clip_width * $ar_width_factor - $step * $step_size );
    }

    my ( $actual_width, $actual_height );
    my ( $zoom_width,   $zoom_height );
    my ( $clip2_width,  $clip2_height );
    my ( $clip2_top,    $clip2_bottom, $clip2_left, $clip2_right );

    if ($target_width) {
        $actual_width  = $target_width;
        $actual_height = int(
            $clip_height - ( $clip_width * $ar_width_factor - $target_width )
                / ( $ar * $height / $clip_height ) );
    }
    else {
        $actual_height = $target_height;
        $actual_width  = int(
            $clip_width * $ar_width_factor - ( $clip_height - $actual_height )
                * ( $ar * $height / $clip_height ) );
    }

    my $zoom_width  = $actual_width;
    my $zoom_height = $actual_height;

    #print "actual_width=$actual_width actual_height=$actual_height\n";

    if ( $zoom_width % $zoom_align ) {
        $zoom_width = int( $zoom_width / $zoom_align + 1 ) * $zoom_align
            if $zoom_width % $zoom_align >= $zoom_align / 2;
        $zoom_width = int( $zoom_width / $zoom_align ) * $zoom_align
            if $zoom_width % $zoom_align < $zoom_align / 2;
    }

    if ( $zoom_height % $zoom_align ) {
        $zoom_height = int( $zoom_height / $zoom_align + 1 ) * $zoom_align
            if $zoom_height % $zoom_align >= $zoom_align / 2;
        $zoom_height = int( $zoom_height / $zoom_align ) * $zoom_align
            if $zoom_height % $zoom_align < $zoom_align / 2;
    }

    #print "zoom_width=$zoom_width zoom_height=$zoom_height\n";

    my $eff_ar = ( $zoom_width / $zoom_height ) * ( $width / $height )
        / ( $clip_width / $clip_height );
    my $ar_err = abs( 100 - $eff_ar / $ar * 100 );

#print "clip_left=$clip_left clip_right=$clip_right clip_top=$clip_top clip_bottom=$clip_bottom\n";

    if ( not $use_clip1 ) {
        $clip2_left  = int( $clip_left * $zoom_width / $clip_width / 2 ) * 2;
        $clip2_right = int( $clip_right * $zoom_width / $clip_width / 2 ) * 2;
        $clip2_top   = int( $clip_top * $zoom_height / $clip_height / 2 ) * 2;
        $clip2_bottom
            = int( $clip_bottom * $zoom_height / $clip_height / 2 ) * 2;
        $result_align_clip2 = 1;
        $result_align = 16 if not defined $result_align;
    }

    $clip2_width  = $zoom_width - $clip2_left - $clip2_right;
    $clip2_height = $zoom_height - $clip2_top - $clip2_bottom;

    #print "clip2_width=$clip2_width clip2_height=$clip2_height\n";

    if ($result_align_clip2) {
        $result_align ||= 16;    # fail safe -> prevent division by zero
        my $rest;
        if ( $rest = $clip2_width % $result_align ) {
            $clip2_left  += $rest / 2;
            $clip2_right += $rest / 2;
            $clip2_width -= $rest;
            if ( $clip2_left % 2 and $clip2_left > $clip2_right ) {
                --$clip2_left;
                ++$clip2_right;
            }
            elsif ( $clip2_left % 2 ) {
                ++$clip2_left;
                --$clip2_right;
            }
        }
        if ( $rest = $clip2_height % $result_align ) {
            $clip2_top    += $rest / 2;
            $clip2_bottom += $rest / 2;
            $clip2_height -= $rest;
            if ( $clip2_top % 2 and $clip2_top > $clip2_bottom ) {
                --$clip2_top;
                ++$clip2_bottom;
            }
            elsif ( $clip2_top % 2 ) {
                ++$clip2_top;
                --$clip2_bottom;
            }
        }
    }

    my $phys_ar = 0;
    $phys_ar = $clip2_width / $clip2_height if $clip2_height != 0;

    # pixels per second
    my $pps = $self->frame_rate * $clip2_width * $clip2_height;

    # bits per pixel
    my $bpp = 0;
    $bpp = $video_bitrate * 1000 / $pps if $pps != 0;

    return {
        zoom_width   => $zoom_width,
        zoom_height  => $zoom_height,
        eff_ar       => $eff_ar,
        ar_err       => $ar_err,
        clip1_left   => ( $clip1_left || 0 ),
        clip1_right  => ( $clip1_right || 0 ),
        clip1_top    => ( $clip1_top || 0 ),
        clip1_bottom => ( $clip1_bottom || 0 ),
        clip1_width  => $width - $clip1_left - $clip1_right,
        clip1_height => $height - $clip1_top - $clip1_bottom,
        clip2_left   => ( $clip2_left || 0 ),
        clip2_right  => ( $clip2_right || 0 ),
        clip2_top    => ( $clip2_top || 0 ),
        clip2_bottom => ( $clip2_bottom || 0 ),
        clip2_width  => $clip2_width,
        clip2_height => $clip2_height,
        phys_ar      => $phys_ar,
        bpp          => $bpp,
        exact_width  => $actual_width,
        exact_height => $actual_height,
    };
}

#---------------------------------------------------------------------
# Methods for Ripping
#---------------------------------------------------------------------

sub is_ripped {
    my $self = shift;

    my $project = $self->project;
    return 1 if $project->rip_mode ne 'rip';

    my $name = $project->name;

    if ( not $self->tc_use_chapter_mode ) {
        my $vob_dir = $self->vob_dir;
        return -f "$vob_dir/$name-001.vob";
    }

    my $chapters = $self->get_chapters;

    my $vob_dir;
    foreach my $chapter ( @{$chapters} ) {
        $self->set_actual_chapter($chapter);
        $vob_dir = $self->vob_dir;
        $self->set_actual_chapter(undef);
        return if not -f "$vob_dir/$name-001.vob";
    }

    return 1;
}

sub get_rip_command {
    my $self = shift;

    my $nr           = $self->tc_title_nr;
    my $name         = $self->project->name;
    my $dvd_device   = $self->project->dvd_device;
    my $vob_dir      = $self->vob_dir;
    my $vob_nav_file = $self->vob_nav_file;

    $self->create_vob_dir;

    my $chapter = $self->tc_use_chapter_mode ? $self->actual_chapter : "-1";

    my $angle = $self->tc_viewing_angle || 1;

    my ( $setup_subtitle_grabbing, $subtitle_grabbing_pipe )
        = $self->get_subtitle_rip_commands;

    my $tc_nice = $self->tc_nice || 0;

    my $command = $setup_subtitle_grabbing
        . "rm -f $vob_dir/$name-???.vob && "
        . "execflow -n $tc_nice tccat -t dvd -T $nr,$chapter,$angle -i $dvd_device "
        . $subtitle_grabbing_pipe
        . "| dvdrip-splitpipe -f $vob_nav_file 1024 "
        . "  $vob_dir/$name vob >/dev/null && echo EXECFLOW_OK";

    return $command;
}

sub set_chapter_length {
    my $self = shift;

    my $chapter      = $self->actual_chapter;
    my $vob_nav_file = $self->vob_nav_file;

    my $fh = FileHandle->new;
    open( $fh, $vob_nav_file )
        or croak __x( "Can't read VOB navigation file '{vob_nav_file}'",
        vob_nav_file => $vob_nav_file );

    my ( $frames, $block_offset, $frame_offset );
    ++$frames while <$fh>;
    close $fh;

    $self->chapter_frames->{$chapter} = $frames;

    1;
}

#---------------------------------------------------------------------
# Methods for Volume Scanning
#---------------------------------------------------------------------

sub get_tc_scan_command_pipe {
    my $self = shift;

    my $audio_channel = $self->audio_channel;
    my $codec         = $self->audio_track->type =~ /pcm/ ? 'pcm' : 'ac3';
    my $tcdecode      = $codec eq 'ac3' ? "| tcdecode -x ac3 " : "";

    my $tc_nice = $self->tc_nice || 0;

    my $command .= "tcextract -a $audio_channel -x $codec -t vob "
        . $tcdecode
        . "| tcscan -x pcm";

    return $command;
}

sub get_scan_command {
    my $self = shift;

    my $nr             = $self->tc_title_nr;
    my $name           = $self->project->name;
    my $data_source    = $self->transcode_data_source;
    my $vob_dir        = $self->vob_dir;
    my $source_options = $self->data_source_options;
    my $rip_mode       = $self->project->rip_mode;
    my $tc_nice        = $self->tc_nice || 0;

    $self->create_vob_dir;

    my $command;

    if ( $rip_mode eq 'rip' ) {
        my $vob_size = $self->get_vob_size;
        $command
            = "execflow -n $tc_nice cat $vob_dir/* | dvdrip-progress -m $vob_size -i 5 | tccat -t vob";

    }
    else {
        $command = "execflow -n $tc_nice tccat ";
        delete $source_options->{x};
        $command .= " -" . $_ . " " . $source_options->{$_}
            for keys %{$source_options};
        $command .= "| dvdrip-splitpipe -f /dev/null 0 - -";
    }

    my $scan_command = $self->get_tc_scan_command_pipe;

    $command .= " | $scan_command";
    $command .= " && echo EXECFLOW_OK";

    return $command;
}

sub get_subtitle_languages {
    my $self = shift;

    my %lang_list;
    foreach my $subtitle ( values %{ $self->subtitles } ) {
        $lang_list{ $subtitle->lang } = 1;
    }

    return \%lang_list;
}

sub has_subtitles {
    my $self = shift;
    return scalar( keys %{ $self->subtitles } ); 
}

sub get_subtitle_rip_commands_spuunmux {
    my $self = shift;

    return if $self->version("spuunmux") < 611;

    my $mode = $self->tc_rip_subtitle_mode;
    return if !$mode;

    my $setup_subtitle_grabbing;
    my $subtitle_grabbing = " | dvdrip-multitee 1 ";

    my $lang = $self->tc_rip_subtitle_lang || [];
    my %lang;
    @lang{ @{$lang} } = (1) x @{$lang};

    my $lang_match;
    foreach my $subtitle ( sort { $a->id <=> $b->id }
        values %{ $self->subtitles } ) {
        next if $mode eq 'lang' && !$lang{ $subtitle->lang };
        $lang_match = 1;
        my $sub_dir = $self->get_subtitle_preview_dir( $subtitle->id );
        my $sid     = $subtitle->id;
        $setup_subtitle_grabbing .= "rm -rf $sub_dir; mkdir -p $sub_dir; "
            . "touch $sub_dir/.ripped; ";
        $subtitle_grabbing .= "'spuunmux -s $sid -o $sub_dir/pic -' ";
    }

    return unless $lang_match;
    return ( $setup_subtitle_grabbing, $subtitle_grabbing );
}

sub get_subtitle_rip_commands {
    my $self = shift;

    my $mode = $self->tc_rip_subtitle_mode;
    return if !$mode;

    my $setup_subtitle_grabbing;
    my $subtitle_grabbing = " | dvdrip-multitee 1 ";

    my $lang = $self->tc_rip_subtitle_lang || [];
    my %lang;
    @lang{ @{$lang} } = (1) x @{$lang};

    my $lang_match;
    foreach my $subtitle ( sort { $a->id <=> $b->id }
        values %{ $self->subtitles } ) {
        next if $mode eq 'lang' && !$lang{ $subtitle->lang };
        $lang_match = 1;
        my $sub_dir = $self->get_subtitle_preview_dir( $subtitle->id );
        my $sid = sprintf( "0x%x", 32 + $subtitle->id );
        $setup_subtitle_grabbing .= "rm -rf $sub_dir; mkdir -p $sub_dir; "
            . "touch $sub_dir/.ripped; ";
        $subtitle_grabbing .= "'tcextract -x ps1 -t vob -a $sid |"
            . " subtitle2pgm -P -C 0 -o $sub_dir/pic -e 00:00:00,50000 2>&1 |"
            . " dvdrip-subpng' ";
    }

    return unless $lang_match;
    return ( $setup_subtitle_grabbing, $subtitle_grabbing );
}

#---------------------------------------------------------------------
# Methods for Ripping and Scanning
#---------------------------------------------------------------------

sub get_rip_and_scan_command {
    my $self = shift;

    my $nr            = $self->tc_title_nr;
    my $audio_channel = $self->audio_channel;
    my $name          = $self->project->name;
    my $dvd_device    = $self->project->dvd_device;
    my $vob_dir       = $self->vob_dir;
    my $vob_nav_file  = $self->vob_nav_file;
    my $tc_nice       = $self->tc_nice || 0;

    $self->create_vob_dir;

    my $chapter = $self->tc_use_chapter_mode ? $self->actual_chapter : "-1";

    my $angle = $self->tc_viewing_angle || 1;

    my ( $setup_subtitle_grabbing, $subtitle_grabbing_pipe )
        = $self->get_subtitle_rip_commands;

    my $command = $setup_subtitle_grabbing
        . "rm -f $vob_dir/$name-???.vob && "
        . "execflow -n $tc_nice tccat -t dvd -T $nr,$chapter,$angle -i $dvd_device "
        . $subtitle_grabbing_pipe
        . "| dvdrip-splitpipe -f $vob_nav_file 1024 $vob_dir/$name vob ";

    if ( $audio_channel != -1 ) {
        my $scan_command = $self->get_tc_scan_command_pipe;
        $command .= " | $scan_command && echo EXECFLOW_OK";

    }
    else {
        $command .= ">/dev/null && echo EXECFLOW_OK";
    }

    return $command;
}

sub analyze_scan_output {
    my $self = shift;
    my %par = @_;
    my ( $output, $count ) = @par{ 'output', 'count' };

    return 1 if $self->audio_channel == -1;

    $output =~ s/^.*?--splitpipe-finished--\n//s;

    Video::DVDRip::Probe->analyze_scan(
        scan_output => $output,
        audio       => $self->audio_track,
        count       => $count,
    );

    1;
}

#---------------------------------------------------------------------
# Methods for Probing DVD
#---------------------------------------------------------------------

sub get_probe_command {
    my $self = shift;

    my $nr          = $self->tc_title_nr;
    my $data_source = $self->project->rip_data_source;

    my $command = "execflow tcprobe -H 10 -i $data_source -T $nr && "
        . "echo EXECFLOW_OK; "
        . "execflow dvdxchap -t $nr $data_source 2>/dev/null";

    return $command;
}

sub analyze_probe_output {
    my $self = shift;
    my %par = @_;
    my ($output) = @par{'output'};

    Video::DVDRip::Probe->analyze(
        probe_output => $output,
        title        => $self,
    );

    1;
}

#---------------------------------------------------------------------
# Methods for probing detailed audio information
#---------------------------------------------------------------------

sub get_probe_audio_command {
    my $self = shift;

    my $nr      = $self->tc_title_nr;
    my $vob_dir = $self->vob_dir;

    my $probe_mb    = 25;
    my $vob_size_mb = $self->get_vob_size;

    $probe_mb = $vob_size_mb - 1 if $probe_mb > $vob_size_mb;

    my $h_option = $probe_mb <= 0 ? "" : "-H $probe_mb";

    return "execflow tcprobe $h_option -i $vob_dir && echo EXECFLOW_OK";
}

sub get_detect_audio_bitrate_command {
    my $self = shift;
    
    my $nr          = $self->tc_title_nr;
    my $tmp_file    = "/tmp/dvdrip.audioprobe.$$.vob";
    my $data_source = $self->project->rip_data_source;

    return
        "execflow tccat -i $data_source -T $nr | ".
        "dvdrip-progress -m 25 -i 1 | ".
        "head -c 25m > $tmp_file && ".
        "tcprobe -i $tmp_file && ".
        "echo EXECFLOW_OK; ".
        "rm -f $tmp_file";
}

sub probe_audio {
    my $self = shift;

    return 1 if $self->audio_channel == -1;

    my $output = $self->system( command => $self->get_probe_audio_command );

    $self->analyze_probe_audio_output( output => $output, );

    1;
}

sub analyze_probe_audio_output {
    my $self = shift;
    my %par = @_;
    my ($output) = @par{'output'};

    Video::DVDRip::Probe->analyze_audio(
        probe_output => $output,
        title        => $self,
    );

    1;
}

#---------------------------------------------------------------------
# Methods for Transcoding
#---------------------------------------------------------------------

sub suggest_transcode_options {
    my $self = shift;
    my ($mode) = @_;

    $mode ||= "all";  # or "update", called after ripping, probing VOB

    my $rip_mode = $self->project->rip_mode;

    if (    $self->video_mode eq 'ntsc'
        and $rip_mode eq 'rip'
        and @{ $self->program_stream_units } > 1 ) {
        $self->set_tc_psu_core(1);
        $self->log(
            __ "Enabled PSU core. Movie is NTSC and has more than one PSU." );

    }
    elsif ( $self->video_mode eq 'ntsc' and $rip_mode eq 'rip' ) {
        $self->log(
            __ "Not enabling PSU core, because this movie has only one PSU."
        );
    }

    if ( $rip_mode eq 'rip' ) {
        if ( $self->tc_use_chapter_mode ) {
            my $chapter = $self->get_first_chapter;
            $self->set_preview_frame_nr(
                int( $self->chapter_frames->{$chapter} / 2 ) );
        }
        else {
            $self->set_preview_frame_nr( int( $self->frames / 2 ) );
        }
    }
    else {
        $self->set_preview_frame_nr(200);
    }

    my $pref_lang = $self->config('preferred_lang');
    if ( $pref_lang =~ /^\s*([a-z]{2})/ ) {
        $pref_lang = $1;
    }
    else {
        $pref_lang = "";
    }

    if ( $pref_lang  ) {
        #-- select the subtitle stream of the preferred language
        #-- with the minumum number of images, because it's likely
        #-- that this is a forced subtitle.
        my $min_image_cnt = 1_000_000;
        my $min_image_subtitle_id;
        foreach my $sid ( sort { $a <=> $b } keys %{ $self->subtitles } ) {
            my $subtitle = $self->subtitles->{$sid};
            if ( $subtitle->lang eq $pref_lang ) {
                if ( $subtitle->ripped_images_cnt < $min_image_cnt ) {
                    $min_image_subtitle_id = $subtitle->id;
                    $min_image_cnt         = $subtitle->ripped_images_cnt;
                }
            }
        }
        if ( defined $min_image_subtitle_id ) {
            $self->set_selected_subtitle_id($min_image_subtitle_id);
        }
    }

    $self->set_tc_video_framerate( $self->frame_rate );

    return if $mode ne 'all';

    if ( $pref_lang  ) {
        foreach my $audio ( @{ $self->audio_tracks } ) {
            if ( $audio->lang eq $pref_lang ) {
                $self->set_audio_channel( $audio->tc_nr );
                last;
            }
        }
    }

    $self->set_skip_video_bitrate_calc(1);

    $self->set_tc_viewing_angle(1) if !$self->tc_viewing_angle;
    $self->set_tc_multipass(1);
    $self->set_tc_keyframe_interval(50);

    my $container = $self->config('default_container');

    # Internal value for MPEG/X*S*VCD/CVD container is 'vcd',
    # but in the Config dialog the more convenient 'mpeg'
    # is used, so this is translated here.
    $container = 'vcd' if $container eq 'mpeg';

    $self->set_tc_container( $self->config('default_container') );
    $self->set_tc_video_codec( $self->config('default_video_codec') );

    if ( $self->tc_video_codec =~ /^(X?S?VCD|CVD)$/ ) {
        $self->set_tc_container('vcd');
    }

    $self->set_preset("auto_medium_fast")
        unless $self->last_applied_preset;

    my $subtitle_langs = $self->get_subtitle_languages;

    if ( $subtitle_langs->{$pref_lang} ) {
        $self->set_tc_rip_subtitle_lang( [$pref_lang] );
    }

    $self->set_tc_video_bitrate_mode("size");
    $self->set_tc_target_size(1400);
    $self->set_tc_disc_size(700);
    $self->set_tc_disc_cnt(2);
    $self->set_tc_video_bitrate_manual(1800);
    $self->set_tc_nice(19);

    if ( $self->config('default_bpp') ne '<none>' ) {
        $self->set_tc_video_bitrate_mode('bpp');
        $self->set_tc_video_bpp_manual( $self->config('default_bpp') );
    }

    $self->set_skip_video_bitrate_calc(0);

    $self->calc_video_bitrate;

    1;
}

sub skip_video_bitrate_calc     { shift->{skip_video_bitrate_calc} }
sub set_skip_video_bitrate_calc { shift->{skip_video_bitrate_calc} = $_[1] }

sub calc_video_bitrate {
    my $self = shift;

    return if $self->skip_video_bitrate_calc;

    my $bc = Video::DVDRip::BitrateCalc->new(
        title      => $self,
        with_sheet => 1,
    );
    $bc->calculate;

    $self->set_tc_video_bpp( $bc->video_bpp );
    $self->set_tc_video_bitrate( $bc->video_bitrate );
    $self->set_storage_video_size( int( $bc->video_size ) );
    $self->set_storage_audio_size( int( $bc->audio_size ) );
    $self->set_storage_other_size( int( $bc->other_size ) );
    $self->set_storage_total_size( int( $bc->file_size ) );

    $self->set_bitrate_calc($bc);

    return $bc->video_bitrate;
}

sub get_first_audio_track {
    my $self = shift;

    return -1 if $self->audio_channel == -1;
    return -1 if not $self->audio_tracks;

    foreach my $audio ( @{ $self->audio_tracks } ) {
        return $audio->tc_nr if $audio->tc_target_track == 0;
    }

    return -1;
}

sub get_last_audio_track {
    my $self = shift;

    return -1 if $self->audio_channel == -1;
    return -1 if not $self->audio_tracks;

    my $tc_nr = -1;
    foreach my $audio ( @{ $self->audio_tracks } ) {
        $tc_nr = $audio->tc_nr if $audio->tc_target_track > $tc_nr;
    }

    return $tc_nr;
}

sub get_additional_audio_tracks {
    my $self = shift;

    my %avi2vob;
    foreach my $audio ( @{ $self->audio_tracks } ) {
        next if $audio->tc_target_track == -1;
        next if $audio->tc_target_track == 0;
        $avi2vob{ $audio->tc_target_track } = $audio->tc_nr;
    }

    return \%avi2vob;
}

sub get_transcode_frame_cnt {
    my $self      = shift;
    my %par       = @_;
    my ($chapter) = @par{'chapter'};

    my $frames_cnt;
    if ( not $chapter ) {
        $frames_cnt = $self->frames;
    }
    else {
        $frames_cnt = $self->chapter_frames->{$chapter};
    }

    my $frames = $frames_cnt;

    if (   $self->tc_start_frame ne ''
        or $self->tc_end_frame ne '' ) {
        $frames = $self->tc_end_frame || $frames_cnt;
        $frames = $frames - $self->tc_start_frame
            if $self->has_vob_nav_file;
        $frames ||= $frames_cnt;
    }

    return $frames;
}

sub get_transcode_progress_max {
    my $self = shift;

    my $subtitle_test = $self->subtitle_test;

    my $chapter = $self->actual_chapter;

    my $progress_max;

    if ($subtitle_test) {
        my ( $from, $to ) = $self->get_subtitle_test_frame_range;
        $progress_max = $to - $from;
    }
    else {
        $progress_max = $self->get_transcode_frame_cnt( chapter => $chapter );
    }

    return $progress_max;
}

sub multipass_log_is_reused {
    my $self = shift;

    return $self->tc_multipass_reuse_log
        && -f $self->multipass_log_dir . "/divx4.log";
}

sub get_transcode_status_option {
    my $self = shift;
    my ($rate) = @_;
    
    $rate ||= 25;
    
    if ( $self->version("transcode") >= 10100) {
        return "--progress_meter 1 --progress_rate $rate";
    }
    else {
        return "--print_status $rate";
    }
}

sub get_transcode_command {
    my $self = shift;
    my %par = @_;
    my ( $pass, $split, $no_audio, $output_file )
        = @par{ 'pass', 'split', 'no_audio', 'output_file' };

    my $bc = Video::DVDRip::BitrateCalc->new( title => $self );
    $bc->calculate;

    my $nr            = $self->nr;
    my $avi_file      = $output_file || $self->avi_file;
    my $audio_channel = $self->get_first_audio_track;

    $audio_channel = -1 if $no_audio;

    my $source_options
        = $self->data_source_options( audio_channel => $audio_channel );

    my ($audio_info);

    if ( $audio_channel != -1 ) {
        $audio_info = $self->audio_tracks->[$audio_channel];
    }

    my $mpeg = 0;
    $mpeg = "svcd" if $self->tc_video_codec =~ /^(X?SVCD|CVD)$/;
    $mpeg = "vcd"  if $self->tc_video_codec =~ /^X?VCD$/;

    my $dir = dirname($avi_file);

    my $tc_nice = $self->tc_nice || 0;
    my $command = "mkdir -p $dir && execflow -n $tc_nice transcode -H 10";

    $command .= " -a $audio_channel" if $audio_channel != -1;

    $command .= " -" . $_ . " " . $source_options->{$_}
        for keys %{$source_options};

    if ( $self->tc_video_bitrate ) {
        $command .= " -w "
            . int( $self->tc_video_bitrate ) . ","
            . $self->tc_keyframe_interval;
    }

    #	if ( not $mpeg ) {
    #		$command .=
    #			" -w ".int($self->tc_video_bitrate);
    #	} elsif ( $mpeg eq 'svcd' and $self->tc_video_bitrate ) {
    #		$command .=
    #			" -w ".int($self->tc_video_bitrate);
    #	}

    if (   $self->tc_start_frame ne ''
        or $self->tc_end_frame ne '' ) {
        my $start_frame = $self->tc_start_frame;
        my $end_frame   = $self->tc_end_frame;
        $start_frame ||= 0;
        $end_frame   ||= $self->frames;

        if ( $start_frame != 0 ) {
            my $options
                = $self->get_frame_grab_options( frame => $start_frame );
            $options->{c} =~ /(\d+)/;
            my $c1 = $1;
            my $c2 = $c1 + $end_frame - $start_frame;
            $command .= " -c $c1-$c2";
            $command .= " -L $options->{L}"
                if $options->{L} ne '';

        }
        else {
            $command .= " -c $start_frame-$end_frame";
        }
    }

    if ($mpeg) {
        my $size            = $bc->disc_size || 1;
        my $reserve_bitrate = $bc->vcd_reserve_bitrate;
        my $mpeg2enc_opts   = "-B $reserve_bitrate -I 0 ";
        if ($split) {
            $mpeg2enc_opts .= "-S $size ";
        }
        else {
            $mpeg2enc_opts .= "-S 10000 ";
        }

        if ( $mpeg eq 'svcd' ) {
            if ( $self->video_mode eq 'pal' ) {
                $mpeg2enc_opts .= " -g 6 -G 15";
            }
            else {
                $mpeg2enc_opts .= " -g 9 -G 18";
                if ( $self->frame_rate == 23.976 ) {
                    $mpeg2enc_opts .= " -p";
                }
            }

            $mpeg2enc_opts = ",'$mpeg2enc_opts'" if $mpeg2enc_opts;

            $command .= " -F 5$mpeg2enc_opts";

            if ( $self->aspect_ratio eq '16:9' ) {

                # 16:9
                if ( $self->last_applied_preset =~ /4_3/ ) {

                    # 4:3 with black bars
                    $command .= " --export_asr 2";
                }
                else {
                    $command .= " --export_asr 3";
                }
            }
            else {

                # 4:3
                $command .= " --export_asr 2";
            }
        }
        else {
            $mpeg2enc_opts = ",'$mpeg2enc_opts'" if $mpeg2enc_opts;

            if ( $self->tc_video_codec eq 'XVCD' ) {
                $command .= " -F 2$mpeg2enc_opts --export_asr 2";
            }
            else {
                $command .= " -F 1$mpeg2enc_opts --export_asr 2";
            }
        }

    }
    else {
        $command .= " -F " . $self->tc_video_af6_codec
            if $self->tc_video_af6_codec ne '';
    }

    if ( $audio_channel != -1 ) {
        $command .= " -d"
            if $audio_info->type eq 'lpcm'
            and $self->version("transcode") < 613;

        if ($mpeg) {
            $command .= " -b " . $audio_info->tc_bitrate;
        }
        elsif ( $audio_info->tc_audio_codec =~ /^mp\d/ ) {
            $command .= " -b "
                . $audio_info->tc_bitrate . ",0,"
                . $audio_info->tc_mp3_quality;
        }
        elsif ( $audio_info->tc_audio_codec eq 'vorbis' ) {
            if ( $audio_info->tc_vorbis_quality_enable ) {
                $command .= " -b 0,1," . $audio_info->tc_vorbis_quality;
            }
            else {
                $command .= " -b " . $audio_info->tc_bitrate;
            }
        }

        if ( $audio_info->tc_audio_codec eq 'ac3' ) {
            $command .= " -A -N " . $audio_info->tc_option_n;

        }
        elsif ( $audio_info->tc_audio_codec eq 'pcm' ) {
            $command .= " -N 0x1";

        }
        else {
            $command .= " -s " . $audio_info->tc_volume_rescale
                if $audio_info->tc_volume_rescale != 0
                and $audio_info->type ne 'lpcm';
            $command .= " --a52_drc_off"
                if $audio_info->tc_audio_filter ne 'a52drc';
            $command .= " -J normalize"
                if $audio_info->tc_audio_filter eq 'normalize';
        }
    }

    if ( $self->version("transcode") >= 613 ) {
        $command .= " --use_rgb -k "
            if not $self->tc_use_yuv_internal;

    }
    elsif ( $self->version("transcode") >= 608 ) {
        $command .= " -V "
            if $self->tc_use_yuv_internal
            and $self->tc_deinterlace ne 'smart'

    }
    else {
        $command .= " -V "
            if $self->tc_use_yuv_internal
            and $self->version("transcode") >= 603;
    }

    $command .= " -C " . $self->tc_anti_alias
        if $self->tc_anti_alias;

    my $fr = $self->tc_video_framerate;

    if ( $self->tc_deinterlace eq '32detect' ) {
        $command .= " -J 32detect=force_mode=3";

    }
    elsif ( $self->tc_deinterlace eq 'smart' ) {
        if ( $self->version("transcode") >= 608 ) {
            $command
                .= " -J smartyuv=threshold=10:Blend=1:diffmode=2:highq=1";
        }
        else {
            $command
                .= " -J smartdeinter=threshold=10:Blend=1:diffmode=2:highq=1";
        }

    }
    elsif ( $self->tc_deinterlace eq 'ivtc' ) {
        $fr = 23.976;
        $command .= " -J ivtc,32detect=force_mode=3,decimate";

    }
    elsif ( $self->tc_deinterlace ) {
        $command .= " -I " . $self->tc_deinterlace;
    }

    if ( $self->tc_video_framerate ) {
        $fr = "24,1" if $fr == 23.976;
        $fr = "30,4" if $fr == 29.97;
        $command .= " -f $fr";
    }

    if ( $self->video_mode eq 'ntsc' and $self->tc_options !~ /-M/ ) {
        my $m = " -M 2";
        $m = " -M 0" if $self->tc_deinterlace eq 'ivtc';
        $command .= $m;
    }

    $command .= " -J preview=xv" if $self->tc_preview;

    my $clip1 = ( $self->tc_clip1_top || 0 ) . ","
        . ( $self->tc_clip1_left   || 0 ) . ","
        . ( $self->tc_clip1_bottom || 0 ) . ","
        . ( $self->tc_clip1_right  || 0 );

    $command .= " -j $clip1"
        if $clip1 =~ /^-?\d+,-?\d+,-?\d+,-?\d+$/
        and $clip1 ne '0,0,0,0';

    my $clip2 = ( $self->tc_clip2_top || 0 ) . ","
        . ( $self->tc_clip2_left   || 0 ) . ","
        . ( $self->tc_clip2_bottom || 0 ) . ","
        . ( $self->tc_clip2_right  || 0 );

    $command .= " -Y $clip2"
        if $clip2 =~ /^-?\d+,-?\d+,-?\d+,-?\d+$/
        and $clip2 ne '0,0,0,0';

    if ( $self->tc_fast_bisection ) {
        $command .= " -r 2,2";

    }
    elsif ( not $self->tc_fast_resize ) {
        my $zoom = $self->tc_zoom_width . "x" . $self->tc_zoom_height;
        $command .= " -Z $zoom"
            if $zoom =~ /^\d+x\d+$/;

    }
    else {
        my $multiple_of = 8;

        my ( $width_n, $height_n, $err_div32, $err_shrink_expand )
            = $self->get_fast_resize_options;

        if ($err_div32) {
            croak __x(
                "When using fast resize: Clip1 and Zoom size must be divisible by {multiple_of}",
                multiple_of => $multiple_of
            );
        }

        if ($err_shrink_expand) {
            croak __
                "When using fast resize: Width and height must both shrink or expand";
        }

        if ( $width_n * $height_n >= 0 ) {
            if ( $width_n > 0 or $height_n > 0 ) {
                $command .= " -X $height_n,$width_n";
                $command .= ",$multiple_of" if $multiple_of != 32;
            }
            elsif ( $width_n < 0 or $height_n < 0 ) {
                $width_n  = abs($width_n);
                $height_n = abs($height_n);
                $command .= " -B $height_n,$width_n";
                $command .= ",$multiple_of" if $multiple_of != 32;
            }
        }
    }

    my $dir = $self->multipass_log_dir;
    $command = "mkdir -m 0775 -p '$dir' && cd $dir && $command";

    if ( $self->tc_multipass ) {
        $command .= " -R $pass";

        $avi_file = "/dev/null" if $pass == 1;

        if ($pass == 1 and not $self->has_vbr_audio
            or (    $pass == 2
                and $self->has_vbr_audio
                and not $self->multipass_log_is_reused )
            ) {
            $command =~ s/(-x\s+[^\s]+)/$1,null/;
            $command =~ s/-x\s+([^,]+),null,null/-x $1,null/;
            $command .= " -y " . $self->tc_video_codec;
            $command .= ",null" if not $self->has_vbr_audio or $pass == 2;
        }

        if ( $pass == 1 and $self->video_mode eq 'ntsc' ) {

            # Don't use -x vob,null with NTSC, because this may
            # cause out-of-sync audio.
            $command =~ s/(-x\s+[^,]+),null/$1/;
        }
    }

    if (   not $self->tc_multipass
        or ( $pass == 2 xor $self->has_vbr_audio )
        or ( $pass == 2 and $self->multipass_log_is_reused ) ) {
        if ($mpeg) {
            $command .= " -y mpeg2enc,mp2enc";
            $command .= " -E " . $audio_info->tc_samplerate
                if $audio_info->tc_samplerate;
        }
        else {
            $command .= " -y " . $self->tc_video_codec;
            if (    $self->tc_container eq 'ogg'
                and $audio_channel != -1 ) {
                $command .= ",ogg"
                    if $audio_info->tc_audio_codec eq 'vorbis';
                $command .= " -m "
                    . $self->target_avi_audio_file(
                    vob_nr => $audio_channel,
                    avi_nr => 0
                    );
            }
            if ( $audio_channel == -1 ) {
                $command .= ",null";

            }
            else {
                if (    not $audio_info->is_passthrough
                    and $audio_info->tc_samplerate != $audio_info->sample_rate
                    and $audio_info->tc_samplerate ) {
                    $command .= " -E " . $audio_info->tc_samplerate
                        if $audio_info->tc_samplerate;
                    $command .= " -J resample"
                        if $audio_info->tc_audio_codec eq 'vorbis';
                }
            }
        }
    }

    if ( $self->tc_psu_core ) {
        $command .= " --psu_mode --nav_seek "
            . $self->vob_nav_file
            . " --no_split ";
    }

    $command .= " -o $avi_file";

    $command .= " ".$self->get_transcode_status_option;

    if ( $self->tc_container eq 'avi' and $self->tc_target_size > 2048 ) {
        $command .= " --avi_limit 9999";
    }

    # Filters
    my $config_strings = $self->tc_filter_settings->get_filter_config_strings;

    foreach my $config ( @{$config_strings} ) {
        next if not $config->{enabled};
        $command .= " -J $config->{filter}";
        $command .= "=$config->{options}" if $config->{options};
    }

    $self->create_avi_dir;

    $command = $self->combine_command_options(
        cmd      => "transcode",
        cmd_line => $command,
        options  => $self->tc_options,
        )
        if $self->tc_options =~ /\S/;

    $command .= $self->get_subtitle_transcode_options;

    $command = "$command && echo EXECFLOW_OK ";

    return $command;
}

sub get_transcode_audio_command {
    my $self = shift;
    my %par  = @_;
    my ( $vob_nr, $target_nr ) = @par{ 'vob_nr', 'target_nr' };

    my $source_options
        = $self->data_source_options( audio_channel => $vob_nr );

    $source_options->{x} = "null,$source_options->{x}";

    my $audio_info = $self->audio_tracks->[$vob_nr];

    my $audio_file = $self->target_avi_audio_file(
        vob_nr => $vob_nr,
        avi_nr => $target_nr
    );

    my $dir = dirname($audio_file);

    my $tc_nice = $self->tc_nice || 0;

    my $command = "mkdir -p $dir && "
        . "execflow -n $tc_nice transcode "
        . " -H 10"
        . " -g 0x0 -u 50"
        . " -a $vob_nr"
        . " -y raw";

    if ( $self->is_ogg ) {
        if ( $audio_info->tc_audio_codec eq 'vorbis' ) {
            $command .= ",ogg -m $audio_file -o /dev/null";
        }
        else {
            $command .= " -m $audio_file -o /dev/null";
        }

    }
    elsif ( $self->tc_container eq 'vcd' ) {
        $command .= ",mp2enc -o $audio_file";

    }
    else {
        $command .= " -o " . $audio_file;
    }

    my ( $k, $v );
    while ( ( $k, $v ) = each %{$source_options} ) {
        $command .= " -$k $v";
    }

    if ( $self->tc_video_framerate ) {
        my $fr = $self->tc_video_framerate;
        $fr = "24,1" if $fr == 23.976;
        $fr = "30,4" if $fr == 29.97;
        $command .= " -f $fr";
    }

    if ( $audio_info->tc_audio_codec eq 'ac3' ) {
        $command .= " -A -N " . $audio_info->tc_option_n;

    }
    elsif ( $audio_info->tc_audio_codec eq 'pcm' ) {
        $command .= " -N 0x1";

    }
    else {

        if ( $audio_info->tc_audio_codec =~ /^mp\d/ ) {
            $command .= " -b "
                . $audio_info->tc_bitrate . ",0,"
                . $audio_info->tc_mp3_quality;

        }
        elsif ( $audio_info->tc_audio_codec eq 'vorbis' ) {
            if ( $audio_info->tc_vorbis_quality_enable ) {
                $command .= " -b 0,1," . $audio_info->tc_vorbis_quality;
            }
            else {
                $command .= " -b " . $audio_info->tc_bitrate;
            }
        }

        $command .= " -s " . $audio_info->tc_volume_rescale
            if $audio_info->tc_volume_rescale != 0;

        $command .= " --a52_drc_off "
            if $audio_info->tc_audio_filter ne 'a52drc';
        $command .= " -J normalize"
            if $audio_info->tc_audio_filter eq 'normalize';

        if (    $audio_info->tc_samplerate != $audio_info->sample_rate
            and $audio_info->tc_samplerate ) {
            $command .= " -E " . $audio_info->tc_samplerate
                if $audio_info->tc_samplerate;
            $command .= " -J resample"
                if $audio_info->tc_audio_codec eq 'vorbis';
        }
    }

    if (   $self->tc_start_frame ne ''
        or $self->tc_end_frame ne '' ) {
        my $start_frame = $self->tc_start_frame;
        my $end_frame   = $self->tc_end_frame;
        $start_frame ||= 0;
        $end_frame   ||= $self->frames;

        if ( $start_frame != 0 ) {
            my $options
                = $self->get_frame_grab_options( frame => $start_frame );
            $options->{c} =~ /(\d+)/;
            my $c1 = $1;
            my $c2 = $c1 + $end_frame - $start_frame;
            $command .= " -c $c1-$c2";
            $command .= " -L $options->{L}"
                if $options->{L} ne '';

        }
        else {
            $command .= " -c $start_frame-$end_frame";
        }
    }

    if ( $self->tc_psu_core ) {
        $command .= " --psu_mode --nav_seek "
            . $self->vob_nav_file
            . " --no_split";
    }

    $command .= " ".$self->get_transcode_status_option;

    $command = $self->combine_command_options(
        cmd      => "transcode",
        cmd_line => $command,
        options  => $self->tc_options,
        )
        if $self->tc_options =~ /\S/;

    if ( $self->tc_container eq 'vcd' ) {
        $command .= " && rm -f " . $self->target_avi_file;
    }

    $command .= " && echo EXECFLOW_OK";

    return $command;
}

sub get_merge_audio_command {
    my $self = shift;
    my %par  = @_;
    my ( $vob_nr, $target_nr ) = @par{ 'vob_nr', 'target_nr' };

    my $avi_file = $self->target_avi_file;
    my $audio_file;
    $audio_file = $self->target_avi_audio_file(
        vob_nr => $vob_nr,
        avi_nr => $target_nr
        )
        if $vob_nr != -1;

    my $command;

    my $tc_nice = $self->tc_nice || 0;

    if ( $self->is_ogg ) {
        $command .= "execflow -n $tc_nice ogmmerge -o $avi_file.merged "
            . " $avi_file"
            . " $audio_file &&"
            . " mv $avi_file.merged $avi_file &&"
            . " rm -f $audio_file &&"
            . " echo EXECFLOW_OK";

    }
    else {
        die "avimerge without audio isn't possible"
            if not $audio_file;

        $command .= "execflow -n $tc_nice avimerge"
            . " -p $audio_file"
            . " -a $target_nr"
            . " -o $avi_file.merged"
            . " -i $avi_file &&"
            . " mv $avi_file.merged $avi_file &&"
            . " rm $audio_file &&"
            . " echo EXECFLOW_OK";
    }

    return $command;
}

sub get_fast_resize_options {
    my $self = shift;

    my $multiple_of = 8;

    my $width  = $self->width - $self->tc_clip1_left - $self->tc_clip1_right;
    my $height = $self->height - $self->tc_clip1_top - $self->tc_clip1_bottom;

    my $zoom_width  = $self->tc_zoom_width;
    my $zoom_height = $self->tc_zoom_height;

    $zoom_width  ||= $width;
    $zoom_height ||= $height;

    my $width_n  = ( $zoom_width - $width ) / $multiple_of;
    my $height_n = ( $zoom_height - $height ) / $multiple_of;

    my ( $err_div32, $err_shrink_expand );

    if ((   $width_n != 0
            and
            ( $zoom_width % $multiple_of != 0 or $width % $multiple_of != 0 )
        )
        or ($height_n != 0
            and (  $zoom_height % $multiple_of != 0
                or $height % $multiple_of != 0 )
        )
        ) {
        $err_div32 = 1;
    }

    if ( $width_n * $height_n < 0 ) {
        $err_shrink_expand = 1;
    }

    return ( $width_n, $height_n, $err_div32, $err_shrink_expand );
}

sub fast_resize_possible {
    my $self = shift;
    my ( undef, undef, $err1, $err2 ) = $self->get_fast_resize_options;
    my $ok = !( $err1 || $err2 );
    $self->set_tc_fast_resize(0) unless $ok;
    return $ok;
}

#---------------------------------------------------------------------
# Methods for MPEG multiplexing
#---------------------------------------------------------------------

sub get_mplex_command {
    my $self = shift;

    my $video_codec = $self->tc_video_codec;

    my $avi_file = $self->target_avi_file;
    my $size     = $self->tc_disc_size;

    my $svcd = $video_codec =~ /^(X?SVCD|CVD)$/;

    my $mplex_f = $svcd ? 4 : $video_codec eq 'XVCD' ? 2 : 1;
    my $mplex_v = $svcd || $video_codec eq 'XVCD' ? "-V" : "";
    my $vext = $svcd ? 'm2v' : 'm1v';

    my $target_file = "$avi_file-%d.mpg";

    my $add_audio_tracks;
    my $add_audio_tracks_href = $self->get_additional_audio_tracks;

    if ( keys %{$add_audio_tracks_href} ) {
        my ( $avi_nr, $vob_nr );
        foreach $avi_nr ( sort keys %{$add_audio_tracks_href} ) {
            $vob_nr = $add_audio_tracks_href->{$avi_nr};
            $add_audio_tracks .= " "
                . $self->target_avi_audio_file(
                vob_nr => $vob_nr,
                avi_nr => $avi_nr,
                )
                . ".mpa";
        }
    }

    my $opt_r;
    if ( $video_codec =~ /^(XS?VCD|CVD)$/ ) {

        #-- get overall bitrate, needed for X(S)VCD.
        my $bc = Video::DVDRip::BitrateCalc->new( title => $self );
        $bc->calculate;
        my $bitrate = $bc->video_bitrate + $bc->audio_bitrate
            + $bc->vcd_reserve_bitrate;
        $opt_r = "-r $bitrate";
    }

    my $tc_nice = $self->tc_nice || 0;

    my $command = "execflow -n $tc_nice mplex -f $mplex_f $opt_r $mplex_v "
        . "-o $target_file $avi_file.$vext $avi_file.mpa "
        . "$add_audio_tracks && echo EXECFLOW_OK";

    return $command;
}

#---------------------------------------------------------------------
# Methods for AVI Splitting
#---------------------------------------------------------------------

sub get_split_command {
    my $self = shift;

    my $avi_file = $self->target_avi_file;
    my $size     = $self->tc_disc_size;

    my $avi_dir = dirname $avi_file;
    $avi_file = basename $avi_file;

    my $split_mask = sprintf( "%s-%03d", $self->project->name, $self->nr, );

    my $command;

    if (    -s "$avi_dir/$avi_file" > 0
        and -s "$avi_dir/$avi_file" <= $size * 1024 * 1024 ) {
        $command = "echo File is smaller than one disc, no need to split. "
            . "&& echo EXECFLOW_OK";
        return $command;
    }

    my $tc_nice = $self->tc_nice || 0;

    if ( $self->is_ogg ) {
        $split_mask .= $self->config('ogg_file_ext');

        $command .= "cd $avi_dir && ls -l && "
            . "execflow -n $tc_nice ogmsplit -s $size $avi_file && "
            . "echo EXECFLOW_OK";
    }
    else {
        $command .= "cd $avi_dir && "
            . "execflow -n $tc_nice avisplit -s $size -i $avi_file -o $split_mask && "
            . "echo EXECFLOW_OK";
    }

    return $command;
}

#---------------------------------------------------------------------
# Methods for taking Snapshots
#---------------------------------------------------------------------

sub snapshot_filename {
    my $self = shift;

    return $self->preview_filename( type => 'orig' );
}

sub raw_snapshot_filename {
    my $self = shift;

    my $raw_filename = $self->snapshot_filename;
    $raw_filename =~ s/\.jpg$/.raw/;

    return $raw_filename;
}

sub get_frame_grab_options {
    my $self    = shift;
    my %par     = @_;
    my ($frame) = @par{'frame'};

    if (   $self->project->rip_mode ne 'rip'
        || !$self->has_vob_nav_file
        || $self->tc_force_slow_grabbing ) {
        $self->log( __ "Fast VOB navigation only available for ripped DVD's, "
                . "falling back to slow method." )
            if $self->project->rip_mode ne 'rip';
        $self->log(
            __ "VOB navigation file is missing. Slow navigation method used."
            )
            if $self->project->rip_mode eq 'rip'
            and not $self->has_vob_nav_file;
        $self->log( __ "Using slow preview grabbing as adviced by user" )
            if $self->tc_force_slow_grabbing;
        return { c => $frame . "-" . ( $frame + 1 ), };
    }

    my $old_chapter = $self->actual_chapter;

    $self->set_actual_chapter( $self->get_first_chapter )
        if $self->tc_use_chapter_mode;

    my $vob_nav_file = $self->vob_nav_file;

    $self->set_actual_chapter($old_chapter)
        if $self->tc_use_chapter_mode;

    my $fh = FileHandle->new;
    open( $fh, $vob_nav_file )
        or croak "msg:"
        . __x( "Can't read VOB navigation file '{vob_nav_file}'",
        vob_nav_file => $vob_nav_file );

    my ( $found, $block_offset, $frame_offset, $psu );

    my $frames = 0;

    while (<$fh>) {
        if ( $frames == $frame ) {
            s/^\s+//;
            s/\s+$//;
            croak "msg:"
                . __x( "VOB navigation file '{vob_nav_file}' is corrupted.",
                vob_nav_file => $vob_nav_file )
                if !/^\d+\s+\d+\s+\d+\s+\d+\s+\d+\s+\d+$/;
            ( $psu, $block_offset, $frame_offset )
                = ( split( /\s+/, $_ ) )[ 0, 4, 5 ];
            $found = 1;
            last;
        }
        ++$frames;
    }

    close $fh;

    croak "msg:"
        . __x(
        "Can't find frame {frame} in VOB navigation file "
            . "'{vob_nav_file}' (which has only {frames} frames). ",
        frame        => $frame,
        vob_nav_file => $vob_nav_file,
        frames       => $frames
        )
        if not $found;

    my @psu;
    if ($psu) {
        @psu = ( S => --$psu );
    }

    return {
        @psu,
        L => $block_offset,
        c => $frame_offset . "-" . ( $frame_offset + 1 )
    };
}

sub get_take_snapshot_command {
    my $self = shift;

    my $nr           = $self->nr;
    my $frame        = $self->preview_frame_nr;
    my $tmp_dir      = "/tmp/dvdrip$$.ppm";
    my $filename     = $self->preview_filename( type => 'orig' );
    my $raw_filename = $self->raw_snapshot_filename;

    my $source_options = $self->data_source_options;

    $source_options->{x} .= ",null";

    my $command = "mkdir -m 0775 $tmp_dir; "
        . "cd $tmp_dir; "
        . "execflow transcode "
        . " -H 10 "
        . $self->get_transcode_status_option
        . ( $self->version("transcode") < 613 ? " -z -k" : "" )
        . " -o snapshot"
        . " -y ppm,null";

    $command .= " -" . $_ . " " . $source_options->{$_}
        for keys %{$source_options};

    my $grab_options = $self->get_frame_grab_options( frame => $frame );

    $command .= " -" . $_ . " " . $grab_options->{$_}
        for keys %{$grab_options};

    $command .= " && "
        . "execflow convert"
        . " -size "
        . $self->width . "x"
        . $self->height
        . " $tmp_dir/snapshot*.ppm $filename && "
        . "execflow convert"
        . " -size "
        . $self->width . "x"
        . $self->height
        . " $tmp_dir/snapshot*.ppm gray:$raw_filename &&"
        . " rm -r $tmp_dir && "
        . "echo EXECFLOW_OK";

    $command =~ s/-x\s+([^,]+),null,null/-x $1,null/;

    return $command;
}

sub calc_snapshot_bounding_box {
    my $self = shift;

    my $filename = $self->raw_snapshot_filename;

    open( IN, $filename )
        or die "can't read '$filename'";
    my $blob = "";
    while (<IN>) {
        $blob .= $_;
    }
    close IN;

    my ( $min_x, $min_y, $max_x, $max_y, $x, $y );
    my $width  = $min_x = $self->width;
    my $height = $min_y = $self->height;
    my $thres  = 12;

    # search min_y
    for ( $x = 0; $x < $width; ++$x ) {
        for ( $y = 0; $y < $height; ++$y ) {
            if (unpack( "C", substr( $blob, $y * $width + $x, 1 ) ) > $thres )
            {
                $min_y = $y if $y < $min_y;
                last;
            }
        }
    }

    # search max_y
    for ( $x = 0; $x < $width; ++$x ) {
        for ( $y = $height - 1; $y >= 0; --$y ) {
            if (unpack( "C", substr( $blob, $y * $width + $x, 1 ) ) > $thres )
            {
                $max_y = $y if $y > $max_y;
                last;
            }
        }
    }

    # search min_x
    for ( $y = 0; $y < $height; ++$y ) {
        for ( $x = 0; $x < $width; ++$x ) {

# print "x=$x y=$y min_x=$min_x c=".unpack("C", substr($blob, $y*$width+$x, 1)),"\n";
            if (unpack( "C", substr( $blob, $y * $width + $x, 1 ) ) > $thres )
            {
                $min_x = $x if $x < $min_x;
                last;
            }
        }
    }

    # search max_y
    for ( $y = 0; $y < $height; ++$y ) {
        for ( $x = $width - 1; $x >= 0; --$x ) {
            if (unpack( "C", substr( $blob, $y * $width + $x, 1 ) ) > $thres )
            {
                $max_x = $x if $x > $max_x;
                last;
            }
        }
    }

    # height clipping must not be odd
    --$min_y if $min_y % 2;
    ++$max_y if $max_y % 2;

    $self->set_bbox_min_x($min_x);
    $self->set_bbox_min_y($min_y);
    $self->set_bbox_max_x($max_x);
    $self->set_bbox_max_y($max_y);

    1;
}

#---------------------------------------------------------------------
# Methods for making clip and zoom images
#---------------------------------------------------------------------

sub make_preview_clip1 {
    my $self = shift;

    return $self->make_preview_clip( type => "clip1", );
}

sub make_preview_clip2 {
    my $self = shift;

    return $self->make_preview_clip( type => "clip2", );
}

sub make_preview_clip {
    my $self = shift;
    my %par = @_;
    my ($type) = @par{'type'};

    my $source_file;
    if ( $type eq 'clip1' ) {
        $source_file = $self->preview_filename( type => 'orig' );
    }
    else {
        $source_file = $self->preview_filename( type => 'zoom' );
    }

    return if not -f $source_file;

    my $target_file = $self->preview_filename( type => $type );

    my $catch = $self->system( command => "identify $source_file" );
    my ( $width, $height );
    ( $width, $height ) = ( $catch =~ /\s+(\d+)x(\d+)\s+/ );

    my ( $top, $bottom, $left, $right );
    if ( $type eq 'clip1' ) {
        $top    = $self->tc_clip1_top;
        $bottom = $self->tc_clip1_bottom;
        $left   = $self->tc_clip1_left;
        $right  = $self->tc_clip1_right;
    }
    else {
        $top    = $self->tc_clip2_top;
        $bottom = $self->tc_clip2_bottom;
        $left   = $self->tc_clip2_left;
        $right  = $self->tc_clip2_right;
    }

    my $new_width  = $width - $left - $right;
    my $new_height = $height - $top - $bottom;

    my $command = "convert $source_file -crop "
        . "${new_width}x${new_height}+$left+$top "
        . $target_file;

    $self->system( command => "convert $source_file -crop "
            . "${new_width}x${new_height}+$left+$top "
            . $target_file );

    1;
}

sub make_preview_zoom {
    my $self = shift;
    my %par = @_;

    my $source_file = $self->preview_filename( type => 'clip1' );
    my $target_file = $self->preview_filename( type => 'zoom' );

    my $new_width  = $self->tc_zoom_width;
    my $new_height = $self->tc_zoom_height;

    if ( not $new_width or not $new_height ) {
        copy( $source_file, $target_file );
        return 1;
    }

    my $catch = $self->system( command => "identify $source_file" );

    $self->system( command => "convert $source_file -geometry "
            . "'${new_width}!x${new_height}!' "
            . $target_file );

    1;
}

sub get_make_preview_command {
    my $self   = shift;
    my %par    = @_;
    my ($type) = @par{'type'};

    my $command;
    if ( $type =~ /clip/ ) {
        my ( $top, $bottom, $left, $right, $source_file );
        if ( $type eq 'clip1' ) {
            $source_file = $self->preview_filename( type => 'orig' );
            $top         = $self->tc_clip1_top;
            $bottom      = $self->tc_clip1_bottom;
            $left        = $self->tc_clip1_left;
            $right       = $self->tc_clip1_right;
        }
        else {
            $source_file = $self->preview_filename( type => 'zoom' );
            $top         = $self->tc_clip2_top;
            $bottom      = $self->tc_clip2_bottom;
            $left        = $self->tc_clip2_left;
            $right       = $self->tc_clip2_right;
        }

        $top    ||= "0";
        $bottom ||= "0";
        $left   ||= "0";
        $right  ||= "0";

        my $target_file = $self->preview_filename( type => $type );
        return "execflow dvdrip-thumb $source_file $target_file "
            . "$top $right $bottom $left";
    }
    elsif ( $type eq 'zoom' ) {
        my $source_file = $self->preview_filename( type => 'clip1' );
        my $target_file = $self->preview_filename( type => 'zoom' );
        my $new_width  = $self->tc_zoom_width  || $self->width;
        my $new_height = $self->tc_zoom_height || $self->height;
        return "execflow dvdrip-thumb $source_file $target_file "
            . "$new_width $new_height";
    }
}

sub get_make_previews_command {
    my $self = shift;

    return $self->get_make_preview_command( type => 'clip1' ) . " && "
        . $self->get_make_preview_command( type  => 'zoom' ) . " && "
        . $self->get_make_preview_command( type  => 'clip2' );
}

#---------------------------------------------------------------------

sub remove_vob_files {
    my $self = shift;

    my $vob_dir = $self->vob_dir;

    unlink(<$vob_dir/*>);

    1;
}

sub get_remove_vobs_command {
    my $self = shift;

    my $vob_dir = $self->vob_dir;

    my $command = "rm $vob_dir/* && echo EXECFLOW_OK";

    return $command;
}

sub get_view_dvd_command {
    my $self           = shift;
    my %par            = @_;
    my ($command_tmpl) = @par{command_tmpl};

    my $nr            = $self->nr;
    my $audio_channel = $self->audio_channel;
    my $base_audio_code;

    if ( $self->audio_track->type eq 'lpcm' ) {
        $base_audio_code = 160;

    }
    elsif ( $self->audio_track->type eq 'mpeg1' ) {
        $base_audio_code = 0;

    }
    else {
        $base_audio_code = 128;
    }

    my @opts = (
        {   t => $self->nr,
            a => $self->audio_channel,
            m => $self->tc_viewing_angle,
            b => $base_audio_code,
            d => $self->project->dvd_device,
        }
    );

    if ( $self->tc_use_chapter_mode eq 'select' ) {
        my $chapters = $self->tc_selected_chapters;
        use Data::Dumper;
        print Dumper($chapters);
        if ( not $chapters or not @{$chapters} ) {
            return "echo 'no chapters selected'";
        }
        push @opts, { c => $_ } foreach @{$chapters};
    }
    else {
        push @opts, { c => 1 };
    }

    my $command = $self->apply_command_template(
        template => $command_tmpl,
        opts     => \@opts,
    );

    return $command;
}

sub get_view_avi_command {
    my $self = shift;
    my %par  = @_;
    my ( $command_tmpl, $file ) = @par{ 'command_tmpl', 'file' };

    my @filenames;
    if ($file) {
        @filenames = ($file);

    }
    elsif ( $self->tc_use_chapter_mode ) {
        my $chapters = $self->get_chapters;
        my $filename;
        foreach my $chapter ( @{$chapters} ) {
            $self->set_actual_chapter($chapter);
            $filename = $self->avi_file;
            push @filenames, $filename if -f $filename;
        }
        $self->set_actual_chapter(undef);

    }
    else {
        my $filename = $self->avi_file;
        my $ext      = $self->get_target_ext;
        $filename =~ s/\.[^.]+$//;
        push @filenames, grep !/dvdrip-info/, glob( "${filename}*" . $ext );
    }

    croak "msg:" . __ "You first have to transcode this title."
        if not @filenames;

    my @opts = ( {} );
    push @opts, { f => $_ } for @filenames;

    my $command = $self->apply_command_template(
        template => $command_tmpl,
        opts     => \@opts,
    );

    return $command;
}

sub get_view_stdin_command {
    my $self           = shift;
    my %par            = @_;
    my ($command_tmpl) = @par{'command_tmpl'};

    my $audio_channel = $self->audio_channel;

    my @opts = ( { a => 0, } );

    my $command = $self->apply_command_template(
        template => $command_tmpl,
        opts     => \@opts,
    );

    my $opts
        = $self->get_frame_grab_options( frame => $self->preview_frame_nr, );

    my $source_options = $self->data_source_options;

    my $T;
    $T = "-T $source_options->{T}" if $source_options->{T};

    $command = "tccat -i $source_options->{i}" . " $T"
        . " -a $audio_channel -S $opts->{L} | $command";

    return $command;
}

sub get_view_vob_image_command {
    my $self           = shift;
    my %par            = @_;
    my ($command_tmpl) = @par{'command_tmpl'};

    my $nr            = $self->nr;
    my $audio_channel = $self->audio_channel;
    my $angle         = $self->tc_viewing_angle;

    my $command = "execflow tccat -i "
        . $self->project->dvd_device
        . " -a $audio_channel -L "
        . " -T $nr,1,$angle | $command_tmpl";

    return $command;
}

#---------------------------------------------------------------------
# CD burning stuff
#---------------------------------------------------------------------

sub get_burn_files {
    my $self = shift;

    my $cd_type = $self->burn_cd_type || 'iso';

    my $ogg_ext = $self->config('ogg_file_ext');

    my $mask =
        $cd_type eq 'iso' ? "*.{avi,$ogg_ext,iso,dvdrip-info,sub,ifo,idx,rar}"
        : $cd_type eq 'vcd' ? "*.{mpg,vcd}"
        : "*.{mpg,svcd}";

    $mask = $self->avi_dir . "/" . $mask;

    my @files = glob($mask);

    my @burn_files;
    my %files_per_group;
    my ($label, $abstract, $base, $group,
        $index, $is_image, $ext,  $chapter
    );

    foreach my $file ( sort @files ) {
        $base = basename($file);

        $base =~ /^(.*?)([_-]\d+)([_-](C?)\d+)?\.([^\.]+)$/;
        $index   = $3;
        $chapter = $4;
        $group   = "$1:$5";

        $base =~ /([^\.]+)$/;
        $ext = $1;

        $index =~ s/C//g;
        $index = $index * -1 if $index < 0;
        ++$files_per_group{$group};

        $is_image = $ext =~ /^(iso|vcd|svcd)$/;
        ++$index
            if $cd_type eq 'iso'
            and not $chapter;    # avi counting begins with 0

        $label = $base;
        $label =~ s/(-C?\d+)*\.[^\.]+$//;

        $abstract = $label;
        $abstract =~ s/_/ /g;
        $abstract =~ s/\b(.)/uc($1)/eg;

        $label .= "_$index" if not $is_image;

        push @burn_files,
            {
            name     => $base,
            label    => $label,
            abstract => $abstract,
            size => ( int( ( -s $file ) / 1024 / 1024 ) || 1 ),
            group    => $group,
            index    => $index,
            path     => $file,
            is_image => $is_image
            };
    }

    foreach my $file (@burn_files) {
        $file->{number}
            = "$file->{index} of " . $files_per_group{ $file->{group} };
    }

    return \@burn_files;
}

sub cd_image_file {
    my $self = shift;

    my $cd_type = $self->burn_cd_type;

    my @labels = map { $_->{label} }
        sort { $a->{label} cmp $b->{label} }
        values %{ $self->burn_files_selected };

    return $self->avi_dir . "/" . $labels[0] . ".$cd_type";
}

sub burning_an_image {
    my $self = shift;

    my $is_image;
    map { $is_image = 1 if $_->{is_image} }
        sort { $a->{path} cmp $b->{path} }
        values %{ $self->burn_files_selected };

    return $is_image;
}

sub get_create_image_command {
    my $self         = shift;
    my %par          = @_;
    my ($on_the_fly) = @par{'on_the_fly'};

    croak "msg:" . __ "No files for image creation selected."
        if not $self->burn_files_selected
        or not keys %{ $self->burn_files_selected };

    my $is_image;
    my @files = map { $is_image = 1 if $_->{is_image}; $_->{path} }
        sort { $a->{path} cmp $b->{path} }
        values %{ $self->burn_files_selected };

    die __ "No burn files selected."      if not @files;
    die __ "File is already an CD image." if $is_image;

    my $cd_type = $self->burn_cd_type;

    if ( $cd_type ne 'iso' and $on_the_fly ) {
        croak __ "Can't burn (S)VCD on the fly";
    }

    my $image_file = $self->cd_image_file;

    my $command;
    if ( $cd_type eq 'iso' ) {
        if ( $on_the_fly and $self->config('burn_estimate_size') ) {
            $command = 'SIZE=$(';
            $command .= $self->config('burn_mkisofs_cmd');
            $command .= " -quiet -print-size"
                . " -r -J -jcharset default -l -D -L" . " -V '"
                . $self->burn_label . "'"
                . " -abstract '"
                . $self->burn_abstract . " "
                . $self->burn_number . "'" . " "
                . join( " ", @files );
            $command .= ") && ";
            $command .= "execflow " . $self->config('burn_mkisofs_cmd');
            $command .= " -quiet";
            $command .= " -r -J -jcharset default -l -D -L" . " -V '"
                . $self->burn_label . "'"
                . " -abstract '"
                . $self->burn_abstract . " "
                . $self->burn_number . "'" . " "
                . join( " ", @files );
        }
        else {
            $command = "execflow " . $self->config('burn_mkisofs_cmd');
            $command .= " -quiet"         if $on_the_fly;
            $command .= " -o $image_file" if not $on_the_fly;
            $command .= " -r -J -jcharset default -l -D -L" . " -V '"
                . $self->burn_label . "'"
                . " -abstract '"
                . $self->burn_abstract . " "
                . $self->burn_number . "'" . " "
                . join( " ", @files );
        }
    }
    else {
        $command = "execflow "
            . $self->config('burn_vcdimager_cmd')
            . ( $cd_type eq 'svcd' ? ' --type=svcd' : ' --type=vcd2' )
            . " --iso-volume-label='"
            . uc( $self->burn_label ) . "'"
            . " --info-album-id='"
            . uc( $self->burn_abstract . " " . $self->burn_number ) . "'"
            . " --cue-file=$image_file.cue"
            . " --bin-file=$image_file" . " "
            . join( " ", @files );
    }

    $command .= " && echo EXECFLOW_OK" if not $on_the_fly;

    return $command;
}

sub get_burn_command {
    my $self = shift;

    croak "msg:" . __ "No files for burning selected."
        if not $self->burn_files_selected
        or not keys %{ $self->burn_files_selected };

    my $cd_type = $self->burn_cd_type;

    my $is_image;
    my @files = map { $is_image = 1 if $_->{is_image}; $_->{path} }
        sort { $a->{path} cmp $b->{path} }
        values %{ $self->burn_files_selected };

    die "msg:" . __ "No burn files selected." if not @files;

    my $command;
    if ( $cd_type eq 'iso' ) {
        if ( not $is_image ) {
            $command = $self->get_create_image_command( on_the_fly => 1 );
            $command .= " | " . $self->config('burn_cdrecord_cmd');
        }
        else {
            $command = "execflow " . $self->config('burn_cdrecord_cmd');
        }

        my $gracetime = $self->config('burn_cdrecord_cmd') =~ /cdrecord/
            ? 'gracetime=5'
            : '';

        $command .= " dev="
            . $self->config('burn_cdrecord_device')
            . " fs=4096k -v -overburn $gracetime"
            . " speed="
            . $self->config('burn_writing_speed')
            . " -eject -pad -overburn";

        $command .= " -dummy" if $self->config('burn_test_mode');

        $command .= ' tsize=${SIZE}s'
            if ( ( not $is_image ) and $self->config('burn_estimate_size') );

        if ( not $is_image ) {
            $command .= " -";
        }
        else {
            $command .= " $files[0]";
        }
    }
    else {
        $command = "rm -f $files[0].bin; ln -s $files[0] $files[0].bin && ";

        $command .= "execflow " . $self->config('burn_cdrdao_cmd');

        if ( $command !~ /\bwrite\b/ ) {
            $command .= " write";
        }

        $command .= " --device "
            . $self->config('burn_cdrecord_device')
            . " --speed "
            . $self->config('burn_writing_speed');

        $command .= " --driver " . $self->config('burn_cdrdao_driver')
            if $self->config('burn_cdrdao_driver');

        $command .= " --buffers " . $self->config('burn_cdrdao_buffers')
            if $self->config('burn_cdrdao_buffers');

        $command .= " --eject"    if $self->config('burn_cdrdao_eject');
        $command .= " --overburn" if $self->config('burn_cdrdao_overburn');
        $command .= " --simulate" if $self->config('burn_test_mode');

        $command .= " $files[0].cue" . " && rm $files[0].bin";
    }

    $command .= " && echo EXECFLOW_OK";

    return $command;
}

sub get_erase_cdrw_command {
    my $self = shift;

    my $blank_method = $self->config('burn_blank_method');
    ($blank_method) = $blank_method =~ /^\s*([^\s]+)/;

    my $command = $self->config('burn_cdrecord_cmd') . " dev="
        . $self->config('burn_cdrecord_device')
        . " blank=$blank_method";

    $command .= " -dummy" if $self->config('burn_test_mode');

    $command .= " && echo EXECFLOW_OK";

    return $command;
}

sub selected_subtitle {
    my $self = shift;
    return undef if not $self->subtitles;
    return undef if not defined $self->selected_subtitle_id;
    return $self->subtitles->{ $self->selected_subtitle_id };
}

sub get_cat_vob_command {
    my $self = shift;

    my $rip_mode = $self->project->rip_mode;

    my $cat;
    if ( $rip_mode eq 'rip' ) {
        $cat = "cat " . $self->vob_dir . "/*";

    }
    else {
        $cat = "execflow tccat -i "
            . $self->project->rip_data_source . " -T "
            . $self->tc_title_nr;
    }

    return $cat;
}

sub get_subtitle_grab_images_command {
    my $self = shift;

    my $subtitle = $self->selected_subtitle;

    my $timecode = $subtitle->tc_preview_timecode;
    my $cnt      = $subtitle->tc_preview_img_cnt;
    my $sid      = sprintf( "0x%02x", $subtitle->id + 32 );

    my $sub_dir = $self->get_subtitle_preview_dir;
    my $vob_dir = $self->vob_dir;

    if ( $timecode !~ /^\d\d:\d\d:\d\d$/ ) {
        my $frames  = $timecode + 0;
        my $seconds = int( $frames / $self->tc_video_framerate );
        $timecode = $self->format_time( time => $seconds );
    }

    $cnt = 0 + $cnt;
    $cnt ||= 1;

    my $cat = $self->get_cat_vob_command;

    my $command = "mkdir -p $sub_dir && rm -f $sub_dir/*.{pgm,srtx} && "
        . " $cat | tcextract -x ps1 -t vob -a $sid |"
        . " subtitle2pgm -P -C 0 -o $sub_dir/pic -v -e $timecode,$cnt"
        . " 2>&1 | dvdrip-subpng && echo EXECFLOW_OK";

    return $command;
}

sub get_frame_of_sec {
    my $self = shift;
    my ($sec) = @_;

    my $frame = int( $sec * $self->frame_rate );

    $frame = 0 if $frame < 0;
    $frame = $self->frames - 1 if $frame >= $self->frames;

    return $frame;
}

sub get_subtitle_test_frame_range {
    my $self = shift;

    my $subtitle    = $self->selected_subtitle;
    my $image_cnt   = $subtitle->tc_test_image_cnt;
    my $first_entry = $subtitle->get_first_entry;
    my $nth_entry   = $subtitle->get_nth_entry($image_cnt);

    my $time_sec_from = $first_entry->get_time_sec;
    my $time_sec_to   = $nth_entry->get_time_sec;

    my $frame_from = $self->get_frame_of_sec( $time_sec_from - 15 );
    my $frame_to   = $self->get_frame_of_sec( $time_sec_to + 15 );

    $frame_to = $frame_from if $frame_to < $frame_from;

    return ( $frame_from, $frame_to );

}

sub get_subtitle_transcode_options {
    my $self = shift;

    my $subtitle = $self->get_render_subtitle;

    return "" if not $subtitle;

    my $command = " -J extsub="
        . $subtitle->id . ":"
        . ( $subtitle->tc_vertical_offset || 0 ) . ":"
        . ( $subtitle->tc_time_shift      || 0 ) . ":"
        . ( $subtitle->tc_antialias   ? "0" : "1" ) . ":"
        . ( $subtitle->tc_postprocess ? "1" : "0" );

    if ( $subtitle->tc_color_manip ) {
        $command .= ":"
            . ( $subtitle->tc_color_a        || 0 ) . ":"
            . ( $subtitle->tc_color_b        || 0 ) . ":"
            . ( $subtitle->tc_assign_color_a || 0 ) . ":"
            . ( $subtitle->tc_assign_color_b || 0 );
    }

    return $command;
}

sub get_subtitle_preview_dir {
    my $self = shift;
    my ($subtitle_id) = @_;

    $subtitle_id = $self->selected_subtitle_id if !defined $subtitle_id;

    if ( $self->tc_use_chapter_mode ) {
        return sprintf( "%s/subtitles/%03d-C%03d/%02d",
            $self->project->snap_dir, $self->nr,
            ( $self->actual_chapter || $self->get_first_chapter || 1 ),
            $subtitle_id );
    }
    else {
        return sprintf( "%s/subtitles/%03d/%02d",
            $self->project->snap_dir, $self->nr, $subtitle_id );
    }
}

sub get_render_subtitle {
    my $self = shift;

    return undef if not $self->subtitles;

    foreach my $subtitle ( values %{ $self->subtitles } ) {
        return $subtitle if $subtitle->tc_render;
    }

    return undef;
}

sub info_file {
    my $self = shift;

    my $info_file = $self->avi_file;

    $info_file =~ s/\.[^.]+$/.dvdrip-info/;
    $info_file .= ".dvdrip-info" if $info_file !~ /\./;

    return $info_file;
}

sub get_transcoded_video_width_height {
    my $self = shift;

    my $width  = $self->tc_zoom_width;
    my $height = $self->tc_zoom_height;

    $width  -= $self->tc_clip2_left + $self->tc_clip2_right;
    $height -= $self->tc_clip2_top + $self->tc_clip2_bottom;

    return ( $width, $height );
}

sub suggest_subtitle_on_black_bars {
    my $self = shift;

    my $subtitle = $self->get_render_subtitle;
    return 1 if not $subtitle;

    croak "msg:" . __ "No subtitle selected" if not $subtitle;

    my $clip2_top    = 0;
    my $clip2_bottom = 0;

    my $width  = $self->tc_zoom_width;
    my $height = $self->tc_zoom_height;

    my $rest = ( $height - $clip2_bottom - $clip2_top ) % 16;

    if ($rest) {
        if ( $rest % 2 ) {
            $clip2_bottom -= int( $rest / 2 ) + 1;
            $clip2_top    -= int( $rest / 2 );
        }
        else {
            $clip2_bottom -= $rest / 2;
            $clip2_top    -= $rest / 2;
        }
    }

    $self->set_tc_clip2_bottom($clip2_bottom);
    $self->set_tc_clip2_top($clip2_top);

    $subtitle->set_tc_vertical_offset(0);

    return 1;
}

sub suggest_subtitle_on_movie {
    my $self = shift;

    my $subtitle = $self->get_render_subtitle;
    return 1 if not $subtitle;

    croak "msg:" . __ "No subtitle selected" if not $subtitle;

    my $clip2_bottom = $self->tc_clip2_bottom;
    my $zoom_height  = $self->tc_zoom_height;
    my $pre_zoom_height
        = $self->height - $self->tc_clip1_top - $self->tc_clip1_bottom;
    my $scale = $pre_zoom_height / $zoom_height;

    my $shift = int( $clip2_bottom * $scale );

    $shift = 0 if $shift < 0;

    $subtitle->set_tc_vertical_offset( $shift + 4 );

    return 1;
}

sub get_extract_ps1_stream_command {
    my $self       = shift;
    my %par        = @_;
    my ($subtitle) = @par{'subtitle'};

    my $vob_size = $self->get_vob_size;
    my $vob_dir  = $self->vob_dir;

    my $sid             = sprintf( "0x%x", 32 + $subtitle->id );
    my $vobsub_ps1_file = $subtitle->ps1_file;
    my $ifo_file        = $subtitle->ifo_file( nr => 0 );

    my $cat = $self->get_cat_vob_command;

    my $command = "$cat | "
        . "dvdrip-progress -m $vob_size -i 5 | "
        . "tcextract -x ps1 -t vob -a $sid > $vobsub_ps1_file && "
        . "echo EXECFLOW_OK";

    return $command;
}

sub get_create_vobsub_command {
    my $self = shift;
    my %par  = @_;
    my ( $subtitle, $start, $end, $file_nr )
        = @par{ 'subtitle', 'start', 'end', 'file_nr' };

    my $avi_dir = $self->avi_dir;

    my $sid = sprintf( "0x%x", 32 + $subtitle->id );
    my $vobsub_prefix   = $subtitle->vobsub_prefix( file_nr => $file_nr );
    my $vobsub_ifo_file = "$vobsub_prefix.ifo";
    my $vobsub_ps1_file = $subtitle->ps1_file;
    my $ifo_file        = $subtitle->ifo_file( nr => 0 );

    my $ps1_size = int( ( -s $vobsub_ps1_file ) / 1024 / 1024 + 1 );

    my $range = "";
    if ( defined $start and defined $end ) {
        $range = "-e $start,$end,0";
        $ps1_size = int( ( $end - $start ) / $self->runtime * $ps1_size + 1 );
        $vobsub_ifo_file = "$vobsub_prefix.ifo";
    }

    my $lang = $subtitle->lang;

    my $command = "mkdir -p $avi_dir && "
        . "cp $ifo_file $avi_dir/$vobsub_ifo_file && "
        . "cd $avi_dir && "
        . "chmod 644 $vobsub_ifo_file && "
        . "execflow cat $vobsub_ps1_file | "
        . "dvdrip-progress -m $ps1_size -i 1 | "
        . "subtitle2vobsub $range"
        . " -i $vobsub_ifo_file "
        . " -o $vobsub_prefix &&"
        . "sed 's/^id: /id: $lang/' < $vobsub_prefix.idx > vobsub$$.tmp && "
        . "mv vobsub$$.tmp $vobsub_prefix.idx && "
        . "echo EXECFLOW_OK";

    if ( $self->has("rar") ) {
        my $rar = $self->config('rar_command');
        $command
            .= " && $rar a $vobsub_prefix $vobsub_prefix.{idx,ifo,sub} && "
            . "rm $vobsub_prefix.{idx,ifo,sub}";
    }

    return $command;
}

sub get_view_vobsub_command {
    my $self       = shift;
    my %par        = @_;
    my ($subtitle) = @par{'subtitle'};

    my $avi_dir = $self->avi_dir;
    my $vob_dir = $self->vob_dir;

    my $vobsub_prefix = $subtitle->vobsub_prefix;

    my $command = "cd $avi_dir && "
        . "mplayer -vobsub $vobsub_prefix -vobsubid 0 $vob_dir/*";

    return $command;
}

sub get_split_files {
    my $self = shift;

    my $mask = $self->avi_file;
    $mask =~ s/\.([^\.]+)$//;
    my $ext = $1;
    $mask .= "-*.$ext";

    my @files = glob($mask);

    return \@files;
}

sub get_count_frames_in_files_command {
    my $self = shift;

    my $files = $self->get_split_files;

    my $command = "echo START";

    foreach my $file ( @{$files} ) {
        if ( $self->is_ogg ) {
            $command .= " && echo 'DVDRIP:OGG:$file' frames=\$(";
            $command .= " ogminfo -v -v $file 2>&1 |"
                . " grep 'v1.*granulepos' | wc -l )";
        }
        else {
            $command .= " && echo 'DVDRIP:AVI:$file' \$(";
            $command .= " tcprobe -H 10 -i $file 2>&1 | grep frames= )";
        }
    }

    $command .= " && echo EXECFLOW_OK";

    return $command;
}

sub has_vobsub_subtitles {
    my $self = shift;

    return 0 if not $self->subtitles;

    foreach my $subtitle ( values %{ $self->subtitles } ) {
        return 1 if $subtitle->tc_vobsub;
    }

    return 0;
}

sub get_create_wav_command {
    my $self = shift;

    return "echo 'No audio channel selected'"
        if $self->audio_channel == -1;

    my $audio_wav_file = $self->audio_wav_file;
    my $dir            = dirname($audio_wav_file);
    my $nr             = $self->nr;
    my $source         = $self->transcode_data_source;
    my $audio_nr       = $self->audio_track->tc_nr;

    my $source_options = $self->data_source_options;
    $source_options->{x} = "null";

    my $tc_nice = $self->tc_nice || 0;

    my $command = "mkdir -p $dir &&"
        . " execflow -n $tc_nice transcode -a $audio_nr "
        . $self->get_transcode_status_option(200)
        . " -y null,wav -u 100 -o $audio_wav_file";

    $command .= " -$_ $source_options->{$_}" for keys %{$source_options};

    $command .= " -d"
        if $self->audio_track->type eq 'lpcm'
        and $self->version("transcode") < 613;

    if (   $self->tc_start_frame ne ''
        or $self->tc_end_frame ne '' ) {
        my $start_frame = $self->tc_start_frame;
        my $end_frame   = $self->tc_end_frame;
        $start_frame ||= 0;
        $end_frame   ||= $self->frames;

        if ( $start_frame != 0 ) {
            my $options
                = $self->get_frame_grab_options( frame => $start_frame );
            $options->{c} =~ /(\d+)/;
            my $c1 = $1;
            my $c2 = $c1 + $end_frame - $start_frame;
            $command .= " -c $c1-$c2";
            $command .= " -L $options->{L}"
                if $options->{L} ne '';

        }
        else {
            $command .= " -c $start_frame-$end_frame";
        }
    }

    $command .= " && echo EXECFLOW_OK";

    return $command;
}

sub check_svcd_geometry {
    my $self = shift;

    return if not $self->tc_container eq 'vcd';

    my $codec = $self->tc_video_codec;
    my $mode  = $self->video_mode;

    return if $codec =~ /^XS?VCD$/;

    my $width
        = ( $self->tc_zoom_width || $self->width ) - $self->tc_clip2_left
        - $self->tc_clip2_right;

    my $height
        = ( $self->tc_zoom_height || $self->height ) - $self->tc_clip2_top
        - $self->tc_clip2_bottom;

    my %valid_values = (
        "VCD:pal:width"  => 352,
        "VCD:pal:height" => 288,

        "VCD:ntsc:width"  => 352,
        "VCD:ntsc:height" => 240,

        "SVCD:pal:width"  => 480,
        "SVCD:pal:height" => 576,

        "SVCD:ntsc:width"  => 480,
        "SVCD:ntsc:height" => 480,

        "CVD:pal:width"  => 352,
        "CVD:pal:height" => 576,

        "CVD:ntsc:width"  => 352,
        "CVD:ntsc:height" => 480,
    );

    my $should_width  = $valid_values{"$codec:$mode:width"};
    my $should_height = $valid_values{"$codec:$mode:height"};

    $mode = uc($mode);

    if ( $width != $should_width or $height != $should_height ) {
        return __x(
            "Your frame size isn't conform to the standard,\n"
                . "which is {should_width}x{should_height} for {codec}/{mode}, "
                . "but you configured {width}x{height}.",
            should_width  => $should_width,
            should_height => $should_height,
            codec         => $codec,
            mode          => $mode,
            width         => $width,
            height        => $height
        );
    }

    return;
}

sub move_clip2_to_clip1 {
    my $self = shift;

    my $clip1_top    = $self->tc_clip1_top;
    my $clip1_bottom = $self->tc_clip1_bottom;
    my $clip1_left   = $self->tc_clip1_left;
    my $clip1_right  = $self->tc_clip1_right;

    if ( $clip1_top or $clip1_bottom or $clip1_left or $clip1_right ) {
        die "msg:"
            . __ "2nd clipping parameters can only be\nmoved to 1st "
            . "clipping parameters, if\n1st clipping is not defined.";
        return 1;
    }

    my $width  = $self->width;
    my $height = $self->height || 1;

    my $zoom_width  = $self->tc_zoom_width  || $self->width;
    my $zoom_height = $self->tc_zoom_height || $self->height;

    my $x_factor = $zoom_width / $width;
    my $y_factor = $zoom_height / $height;

    my $clip2_top    = $self->tc_clip2_top;
    my $clip2_bottom = $self->tc_clip2_bottom;
    my $clip2_left   = $self->tc_clip2_left;
    my $clip2_right  = $self->tc_clip2_right;

    my $clip1_top    = $clip2_top / $y_factor;
    my $clip1_bottom = $clip2_bottom / $y_factor;
    my $clip1_left   = $clip2_left / $x_factor;
    my $clip1_right  = $clip2_right / $x_factor;

    $width  = $width - $clip1_left - $clip1_right;
    $height = $height - $clip1_top - $clip1_bottom;

    $zoom_width  = $width * $x_factor;
    $zoom_height = $height * $y_factor;

    # no odd clip values
    if ( $clip1_left % 2 and $clip1_right % 2 ) {
        if ( $clip1_left > $clip1_right ) {
            --$clip1_left;
            ++$clip1_right;
        }
        else {
            ++$clip1_left;
            --$clip1_right;
        }
    }
    else {
        --$clip1_left  if $clip1_left % 2;
        --$clip1_right if $clip1_right % 2;
    }

    if ( $clip1_top % 2 and $clip1_bottom % 2 ) {
        if ( $clip1_left > $clip1_bottom ) {
            --$clip1_top;
            ++$clip1_bottom;
        }
        else {
            ++$clip1_top;
            --$clip1_bottom;
        }
    }
    else {
        --$clip1_top    if $clip1_top % 2;
        --$clip1_bottom if $clip1_bottom % 2;
    }

    $self->set_tc_clip1_top( int($clip1_top) );
    $self->set_tc_clip1_bottom( int($clip1_bottom) );
    $self->set_tc_clip1_left( int($clip1_left) );
    $self->set_tc_clip1_right( int($clip1_right) );
    $self->set_tc_zoom_width( int($zoom_width) );
    $self->set_tc_zoom_height( int($zoom_height) );
    $self->set_tc_clip2_top(0);
    $self->set_tc_clip2_bottom(0);
    $self->set_tc_clip2_left(0);
    $self->set_tc_clip2_right(0);

    1;
}

1;
