File size: 7,331 Bytes
4b12e15
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
# Copyright (c) 2025 Stephen G. Pope
#
# This program is free software; you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation; either version 2 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License along
# with this program; if not, write to the Free Software Foundation, Inc.,
# 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.



import os
import json
import subprocess
import logging
import uuid
from services.file_management import download_file
from services.cloud_storage import upload_file
from config import LOCAL_STORAGE_PATH

# Set up logging
logger = logging.getLogger(__name__)
logging.basicConfig(level=logging.INFO)

def time_to_seconds(time_str):
    """
    Convert a time string in format HH:MM:SS[.mmm] to seconds.
    
    Args:
        time_str (str): Time string
        
    Returns:
        float: Time in seconds
    """
    try:
        parts = time_str.split(':')
        if len(parts) == 3:
            hours, minutes, seconds = parts
            return int(hours) * 3600 + int(minutes) * 60 + float(seconds)
        elif len(parts) == 2:
            minutes, seconds = parts
            return int(minutes) * 60 + float(seconds)
        else:
            return float(time_str)
    except ValueError:
        raise ValueError(f"Invalid time format: {time_str}. Expected HH:MM:SS[.mmm]")

def split_video(video_url, splits, job_id=None, video_codec='libx264', video_preset='medium', 
               video_crf=23, audio_codec='aac', audio_bitrate='128k'):
    """
    Splits a video file into multiple segments with customizable encoding settings.
    
    Args:
        video_url (str): URL of the video file to split
        splits (list): List of dictionaries with 'start' and 'end' timestamps
        job_id (str, optional): Unique job identifier
        video_codec (str, optional): Video codec to use for encoding (default: 'libx264')
        video_preset (str, optional): Encoding preset for speed/quality tradeoff (default: 'medium')
        video_crf (int, optional): Constant Rate Factor for quality (0-51, default: 23)
        audio_codec (str, optional): Audio codec to use for encoding (default: 'aac')
        audio_bitrate (str, optional): Audio bitrate (default: '128k')
        
    Returns:
        tuple: (list of output file paths, input file path)
    """
    logger.info(f"Starting video split operation for {video_url}")
    if not job_id:
        job_id = str(uuid.uuid4())
        
    input_filename = download_file(video_url, os.path.join(LOCAL_STORAGE_PATH, f"{job_id}_input"))
    logger.info(f"Downloaded video to local file: {input_filename}")
    
    output_files = []
    
    try:
        # Get the file extension
        _, ext = os.path.splitext(input_filename)
        
        # Get the duration of the input file
        probe_cmd = [
            'ffprobe', 
            '-v', 'error', 
            '-show_entries', 'format=duration', 
            '-of', 'default=noprint_wrappers=1:nokey=1',
            input_filename
        ]
        duration_result = subprocess.run(probe_cmd, capture_output=True, text=True)
        
        try:
            file_duration = float(duration_result.stdout.strip())
            logger.info(f"File duration: {file_duration} seconds")
        except (ValueError, IndexError):
            logger.warning("Could not determine file duration, using a large value")
            file_duration = 86400  # 24 hours as a fallback
        
        # Validate and process splits
        valid_splits = []
        for i, split in enumerate(splits):
            try:
                start_seconds = time_to_seconds(split['start'])
                end_seconds = time_to_seconds(split['end'])
                
                # Validate split times
                if start_seconds >= end_seconds:
                    logger.warning(f"Invalid split {i+1}: start time ({split['start']}) must be before end time ({split['end']}). Skipping.")
                    continue
                
                if start_seconds < 0:
                    logger.warning(f"Split {i+1} start time {split['start']} is negative, using 0 instead")
                    start_seconds = 0
                    
                if end_seconds > file_duration:
                    logger.warning(f"Split {i+1} end time {split['end']} exceeds file duration, using file duration instead")
                    end_seconds = file_duration
                    
                # Only add valid splits
                if start_seconds < end_seconds:
                    valid_splits.append((i, start_seconds, end_seconds, split))
            except ValueError as e:
                logger.warning(f"Error processing split {i+1}: {str(e)}. Skipping.")
        
        if not valid_splits:
            raise ValueError("No valid split segments specified")
            
        logger.info(f"Processing {len(valid_splits)} valid splits")
        
        # Process each split
        for index, (split_index, start_seconds, end_seconds, split_data) in enumerate(valid_splits):
            # Create output filename for this split
            output_filename = os.path.join(LOCAL_STORAGE_PATH, f"{job_id}_split_{index+1}{ext}")
            
            # Create FFmpeg command to extract the segment
            cmd = [
                'ffmpeg',
                '-i', input_filename,
                '-ss', str(start_seconds),
                '-to', str(end_seconds),
                '-c:v', video_codec,
                '-preset', video_preset,
                '-crf', str(video_crf),
                '-c:a', audio_codec,
                '-b:a', audio_bitrate,
                '-avoid_negative_ts', 'make_zero',
                output_filename
            ]
            
            logger.info(f"Running FFmpeg command for split {index+1}: {' '.join(cmd)}")
            
            # Run the FFmpeg command
            process = subprocess.run(cmd, capture_output=True, text=True)
            
            if process.returncode != 0:
                logger.error(f"Error processing split {index+1}: {process.stderr}")
                raise Exception(f"FFmpeg error for split {index+1}: {process.stderr}")
            
            # Add the output file to the list
            output_files.append(output_filename)
            logger.info(f"Successfully created split {index+1}: {output_filename}")
        
        # Return the list of output files and the input filename
        return output_files, input_filename
        
    except Exception as e:
        logger.error(f"Video split operation failed: {str(e)}")
        
        # Clean up all temporary files if they exist
        if 'input_filename' in locals() and os.path.exists(input_filename):
            os.remove(input_filename)
                
        for output_file in output_files:
            if os.path.exists(output_file):
                os.remove(output_file)
                
        raise