File size: 8,612 Bytes
c93c8fd
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
3de7ae7
c93c8fd
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
840b227
c93c8fd
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
6775f15
 
 
 
 
 
 
 
 
 
a1d40e7
6775f15
 
 
 
 
a1d40e7
c93c8fd
 
 
 
 
 
 
 
 
 
 
 
 
 
 
3de7ae7
c93c8fd
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
3de7ae7
c93c8fd
 
 
 
 
 
699d3d4
 
 
 
c93c8fd
699d3d4
 
 
c93c8fd
699d3d4
 
 
 
c93c8fd
699d3d4
 
 
 
c93c8fd
699d3d4
 
c93c8fd
 
 
841b38e
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
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
import os
import subprocess
import pathlib
# from fastapi import HTTPException
import asyncio

# Define base directories
BASE_DIR = pathlib.Path('./videos').resolve()
HLS_DIR = pathlib.Path('./hls_videos').resolve()
HLS_DIR.mkdir(exist_ok=True)


# Keep track of video names added to the queue
video_names = []
segment_counter = 1
segment_lock = asyncio.Lock()

def is_valid_path(video_name):
    """
    Validates the video path to prevent directory traversal attacks.

    Args:
        video_name (str): Name of the video file.

    Returns:
        bool: True if valid, False otherwise.
    """
    video_path = (BASE_DIR / video_name).resolve()
    return str(video_path).startswith(str(BASE_DIR))

def convert_to_hls(input_path, output_dir, video_name, start_number):
    """
    Converts an MP4 video to HLS format with 3-second segments starting from `start_number`.

    Args:
        input_path (str): Path to the input MP4 video.
        output_dir (str): Directory where HLS files will be stored.
        video_name (str): Name of the video (used for playlist naming).
        start_number (int): The starting number for HLS segments.

    Returns:
        int: Number of segments generated.
    """
    # FFmpeg command to convert MP4 to HLS with 3-second segments
    cmd = [
        'ffmpeg',
        '-y',  # Overwrite output files without asking
        '-i', input_path,  # Input file
        #'-codec', 'copy',  # Audio codec-vf scale=1280:720 -r 30 -c:v libx264 -b:v 1500k -c:a aac -b:a 128k
        '-vf','scale=1280:720',
        '-r','30',
        '-c:v','libx264',
        '-b:v','1500k',
        '-c:a','aac',
        '-b:a','128k',
        '-hls_time', '3',  # Segment duration in seconds
        '-hls_playlist_type', 'vod',  # Video on Demand playlist type
        '-start_number', str(start_number),  # Starting segment number
        '-hls_segment_filename', os.path.join(output_dir, 'video_stream%d.ts'),  # Segment file naming
        os.path.join(output_dir, f'{video_name}.m3u8')  # Output playlist
    ]

    try:
        # Execute the FFmpeg command
        subprocess.run(cmd, check=True, stderr=subprocess.PIPE)
    except subprocess.CalledProcessError as e:
        # Log FFmpeg errors and raise an HTTP exception
        error_message = e.stderr.decode()
        print(f"FFmpeg error: {error_message}")
        raise HTTPException(status_code=500, detail="Video conversion failed")



async def add_video(video_name, m3u8_file_path):
    """
    Endpoint to add a video to the streaming queue.

    Args:
        video_name (str): Name of the video file to add.
        request (Request): FastAPI request object to extract base URL.

    Returns:
        dict: Success message.
    """
    
    print((HLS_DIR / f'{video_name}.m3u8').exists())
    if not (HLS_DIR / f'{video_name}.m3u8').exists():
        async with segment_lock:
            global segment_counter
            current_start_number = segment_counter
            num_segments_before = len([f for f in os.listdir(HLS_DIR) if f.startswith('video_stream') and f.endswith('.ts')])
            await asyncio.get_event_loop().run_in_executor(
                None, convert_to_hls, str(video_name), str(HLS_DIR), video_name, current_start_number
            )
            num_segments_after = len([f for f in os.listdir(HLS_DIR) if f.startswith('video_stream') and f.endswith('.ts')])
            num_new_segments = num_segments_after - num_segments_before
            # Update the global segment counter
            segment_counter += num_new_segments

    video_names.append(video_name)

    existing_segments = set()
    if m3u8_file_path.exists():
        with open(m3u8_file_path, 'r') as m3u8_file:
            for line in m3u8_file:
                if line.startswith('/live_stream/video_stream'):
                    existing_segments.add(line.strip())

    new_ts_files = sorted([f for f in os.listdir(HLS_DIR) if f.startswith('video_stream') and f.endswith('.ts')])

    with open(m3u8_file_path, 'a') as m3u8_file:
        for ts_file in new_ts_files[-num_new_segments:]:  # Only process the new segments
            segment_path = f"/live_stream/{ts_file}"
            if segment_path not in existing_segments:  # Check if already exists
                m3u8_file.write(f"#EXTINF:3.000,\n")  # Adjust duration as necessary
                m3u8_file.write(f"{segment_path}\n")
    
    # Update concatenated playlist
    #await concatenate_playlists(video_names, HLS_DIR)

    return {"message": f'"{video_name}" added to the streaming queue.'}

async def concatenate_playlists(video_names, base_dir):
    """
    Concatenates multiple HLS playlists into a single playlist with unique segment numbering.

    Args:
        video_names (list): List of video names added to the queue.
        base_dir (str): Base directory where HLS files are stored.
        request (Request): FastAPI request object to extract base URL.
    """
    concatenated_playlist_path = os.path.join(base_dir, 'master.m3u8')
    max_segment_duration = 3  # Since we set hls_time to 3
    segment_lines = []  # To store segment lines

    # Construct base URL from the incoming request
    

    for video_name in video_names:
        video_playlist_path = os.path.join(base_dir, f'{video_name}.m3u8')
        if os.path.exists(video_playlist_path):
            with open(video_playlist_path, 'r') as infile:
                lines = infile.readlines()
                for line in lines:
                    line = line.strip()
                    if line.startswith('#EXTINF'):
                        # Append EXTINF line
                        segment_lines.append(line)
                    elif line.endswith('.ts'):
                        segment_file = line
                        # Update segment URI to include full URL
                        segment_path = f'{segment_file}'
                        segment_lines.append(segment_path)
                    elif line.startswith('#EXT-X-BYTERANGE'):
                        # Include byte range if present
                        segment_lines.append(line)
                    elif line.startswith('#EXT-X-ENDLIST'):
                        # Do not include this here; we'll add it at the end
                        continue
                    elif line.startswith('#EXTM3U') or line.startswith('#EXT-X-VERSION') or line.startswith('#EXT-X-PLAYLIST-TYPE') or line.startswith('#EXT-X-TARGETDURATION') or line.startswith('#EXT-X-MEDIA-SEQUENCE'):
                        # Skip these tags; they'll be added in the concatenated playlist
                        continue
                    else:
                        # Include any other necessary tags
                        segment_lines.append(line)

    # Write the concatenated playlist
    with open(concatenated_playlist_path, 'w') as outfile:
        outfile.write('#EXTM3U\n')
        outfile.write('#EXT-X-PLAYLIST-TYPE:VOD\n')
        outfile.write(f'#EXT-X-TARGETDURATION:{max_segment_duration}\n')
        outfile.write('#EXT-X-VERSION:4\n')
        outfile.write(f'#EXT-X-MEDIA-SEQUENCE:{1}\n')  # Starting from segment number 1
        for line in segment_lines:
            outfile.write(f'{line}\n')
        outfile.write('#EXT-X-ENDLIST\n')

def generate_m3u8(video_duration, output_path,segment_duration=3):
    # Initialize playlist content
    m3u8_content = "#EXTM3U\n"
    m3u8_content += "#EXT-X-PLAYLIST-TYPE:VOD\n"
    m3u8_content += f"#EXT-X-TARGETDURATION:{segment_duration}\n"
    m3u8_content += "#EXT-X-VERSION:4\n"
    m3u8_content += "#EXT-X-MEDIA-SEQUENCE:1\n"
    # m3u8_content += f"#EXTINF:{segment_duration:.6f},\n"
    # m3u8_content += f"/live_stream/video_stream{i + 1}.ts\n"
    
    # # Calculate the number of full segments and the remaining duration

    # segment_duration = int(segment_duration)
    # full_segments = int(video_duration // segment_duration)
    # remaining_duration = video_duration % segment_duration

    # # Add full segments to the playlist
    # for i in range(full_segments):
    #     m3u8_content += f"#EXTINF:{segment_duration:.6f},\n"
    #     m3u8_content += f"/live_stream/video_stream{i + 1}.ts\n"

    # # Add the remaining segment if there's any leftover duration
    # if remaining_duration > 0:
    #     m3u8_content += f"#EXTINF:{remaining_duration:.6f},\n"
    #     m3u8_content += f"/live_stream/video_stream{full_segments + 1}.ts\n"

    # # End the playlist
    # m3u8_content += "#EXT-X-ENDLIST\n"
    with open(output_path, "w") as file:
        file.write(m3u8_content)    

    print(f"M3U8 playlist saved to {output_path}")