Spanicin commited on
Commit
c93c8fd
·
verified ·
1 Parent(s): 932d297

Update stream_server.py

Browse files
Files changed (1) hide show
  1. stream_server.py +189 -189
stream_server.py CHANGED
@@ -1,190 +1,190 @@
1
- import os
2
- import subprocess
3
- import pathlib
4
- # from fastapi import HTTPException
5
- import asyncio
6
-
7
- # Define base directories
8
- BASE_DIR = pathlib.Path('./videos').resolve()
9
- HLS_DIR = pathlib.Path('./hls_videos').resolve()
10
- HLS_DIR.mkdir(exist_ok=True)
11
-
12
-
13
- # Keep track of video names added to the queue
14
- video_names = []
15
- segment_counter = 1
16
- segment_lock = asyncio.Lock()
17
-
18
- def is_valid_path(video_name):
19
- """
20
- Validates the video path to prevent directory traversal attacks.
21
-
22
- Args:
23
- video_name (str): Name of the video file.
24
-
25
- Returns:
26
- bool: True if valid, False otherwise.
27
- """
28
- video_path = (BASE_DIR / video_name).resolve()
29
- return str(video_path).startswith(str(BASE_DIR))
30
-
31
- def convert_to_hls(input_path, output_dir, video_name, start_number):
32
- """
33
- Converts an MP4 video to HLS format with 3-second segments starting from `start_number`.
34
-
35
- Args:
36
- input_path (str): Path to the input MP4 video.
37
- output_dir (str): Directory where HLS files will be stored.
38
- video_name (str): Name of the video (used for playlist naming).
39
- start_number (int): The starting number for HLS segments.
40
-
41
- Returns:
42
- int: Number of segments generated.
43
- """
44
- # FFmpeg command to convert MP4 to HLS with 3-second segments
45
- cmd = [
46
- 'ffmpeg',
47
- '-y', # Overwrite output files without asking
48
- '-i', input_path, # Input file
49
- #'-codec', 'copy', # Audio codec-vf scale=1280:720 -r 30 -c:v libx264 -b:v 1500k -c:a aac -b:a 128k
50
- '-vf','scale=1280:720',
51
- '-r','30',
52
- '-c:v','libx264',
53
- '-b:v','1500k',
54
- '-c:a','aac',
55
- '-b:a','128k',
56
- '-hls_time', '3', # Segment duration in seconds
57
- '-hls_playlist_type', 'vod', # Video on Demand playlist type
58
- '-start_number', str(start_number), # Starting segment number
59
- '-hls_segment_filename', os.path.join(output_dir, 'video_stream%d.ts'), # Segment file naming
60
- os.path.join(output_dir, f'{video_name}.m3u8') # Output playlist
61
- ]
62
-
63
- try:
64
- # Execute the FFmpeg command
65
- subprocess.run(cmd, check=True, stderr=subprocess.PIPE)
66
- except subprocess.CalledProcessError as e:
67
- # Log FFmpeg errors and raise an HTTP exception
68
- error_message = e.stderr.decode()
69
- print(f"FFmpeg error: {error_message}")
70
- raise HTTPException(status_code=500, detail="Video conversion failed")
71
-
72
-
73
-
74
- async def add_video(video_name: str):
75
- """
76
- Endpoint to add a video to the streaming queue.
77
-
78
- Args:
79
- video_name (str): Name of the video file to add.
80
- request (Request): FastAPI request object to extract base URL.
81
-
82
- Returns:
83
- dict: Success message.
84
- """
85
-
86
- print((HLS_DIR / f'{video_name}.m3u8').exists())
87
- if not (HLS_DIR / f'{video_name}.m3u8').exists():
88
- async with segment_lock:
89
- global segment_counter
90
- current_start_number = segment_counter
91
- num_segments_before = len([f for f in os.listdir(HLS_DIR) if f.startswith('video_stream') and f.endswith('.ts')])
92
- await asyncio.get_event_loop().run_in_executor(
93
- None, convert_to_hls, str(video_name), str(HLS_DIR), video_name, current_start_number
94
- )
95
- num_segments_after = len([f for f in os.listdir(HLS_DIR) if f.startswith('video_stream') and f.endswith('.ts')])
96
- num_new_segments = num_segments_after - num_segments_before
97
- # Update the global segment counter
98
- segment_counter += num_new_segments
99
-
100
- video_names.append(video_name)
101
- # Update concatenated playlist
102
- #await concatenate_playlists(video_names, HLS_DIR)
103
-
104
- return {"message": f'"{video_name}" added to the streaming queue.'}
105
-
106
- async def concatenate_playlists(video_names, base_dir):
107
- """
108
- Concatenates multiple HLS playlists into a single playlist with unique segment numbering.
109
-
110
- Args:
111
- video_names (list): List of video names added to the queue.
112
- base_dir (str): Base directory where HLS files are stored.
113
- request (Request): FastAPI request object to extract base URL.
114
- """
115
- concatenated_playlist_path = os.path.join(base_dir, 'master.m3u8')
116
- max_segment_duration = 3 # Since we set hls_time to 3
117
- segment_lines = [] # To store segment lines
118
-
119
- # Construct base URL from the incoming request
120
-
121
-
122
- for video_name in video_names:
123
- video_playlist_path = os.path.join(base_dir, f'{video_name}.m3u8')
124
- if os.path.exists(video_playlist_path):
125
- with open(video_playlist_path, 'r') as infile:
126
- lines = infile.readlines()
127
- for line in lines:
128
- line = line.strip()
129
- if line.startswith('#EXTINF'):
130
- # Append EXTINF line
131
- segment_lines.append(line)
132
- elif line.endswith('.ts'):
133
- segment_file = line
134
- # Update segment URI to include full URL
135
- segment_path = f'{segment_file}'
136
- segment_lines.append(segment_path)
137
- elif line.startswith('#EXT-X-BYTERANGE'):
138
- # Include byte range if present
139
- segment_lines.append(line)
140
- elif line.startswith('#EXT-X-ENDLIST'):
141
- # Do not include this here; we'll add it at the end
142
- continue
143
- 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'):
144
- # Skip these tags; they'll be added in the concatenated playlist
145
- continue
146
- else:
147
- # Include any other necessary tags
148
- segment_lines.append(line)
149
-
150
- # Write the concatenated playlist
151
- with open(concatenated_playlist_path, 'w') as outfile:
152
- outfile.write('#EXTM3U\n')
153
- outfile.write('#EXT-X-PLAYLIST-TYPE:VOD\n')
154
- outfile.write(f'#EXT-X-TARGETDURATION:{max_segment_duration}\n')
155
- outfile.write('#EXT-X-VERSION:4\n')
156
- outfile.write(f'#EXT-X-MEDIA-SEQUENCE:{1}\n') # Starting from segment number 1
157
- for line in segment_lines:
158
- outfile.write(f'{line}\n')
159
- outfile.write('#EXT-X-ENDLIST\n')
160
-
161
- def generate_m3u8(video_duration, output_path,segment_duration=3):
162
- # Initialize playlist content
163
- m3u8_content = "#EXTM3U\n"
164
- m3u8_content += "#EXT-X-PLAYLIST-TYPE:VOD\n"
165
- m3u8_content += f"#EXT-X-TARGETDURATION:{segment_duration}\n"
166
- m3u8_content += "#EXT-X-VERSION:4\n"
167
- m3u8_content += "#EXT-X-MEDIA-SEQUENCE:1\n"
168
-
169
- # Calculate the number of full segments and the remaining duration
170
-
171
- segment_duration = int(segment_duration)
172
- full_segments = int(video_duration // segment_duration)
173
- remaining_duration = video_duration % segment_duration
174
-
175
- # Add full segments to the playlist
176
- for i in range(full_segments):
177
- m3u8_content += f"#EXTINF:{segment_duration:.6f},\n"
178
- m3u8_content += f"/live_stream/video_stream{i + 1}.ts\n"
179
-
180
- # Add the remaining segment if there's any leftover duration
181
- if remaining_duration > 0:
182
- m3u8_content += f"#EXTINF:{remaining_duration:.6f},\n"
183
- m3u8_content += f"/live_stream/video_stream{full_segments + 1}.ts\n"
184
-
185
- # End the playlist
186
- m3u8_content += "#EXT-X-ENDLIST\n"
187
- with open(output_path, "w") as file:
188
- file.write(m3u8_content)
189
-
190
  print(f"M3U8 playlist saved to {output_path}")
 
1
+ import os
2
+ import subprocess
3
+ import pathlib
4
+ # from fastapi import HTTPException
5
+ import asyncio
6
+
7
+ # Define base directories
8
+ BASE_DIR = pathlib.Path('./videos').resolve()
9
+ HLS_DIR = pathlib.Path('./hls_videos').resolve()
10
+ HLS_DIR.mkdir(exist_ok=True)
11
+
12
+
13
+ # Keep track of video names added to the queue
14
+ video_names = []
15
+ segment_counter = 1
16
+ segment_lock = asyncio.Lock()
17
+
18
+ def is_valid_path(video_name):
19
+ """
20
+ Validates the video path to prevent directory traversal attacks.
21
+
22
+ Args:
23
+ video_name (str): Name of the video file.
24
+
25
+ Returns:
26
+ bool: True if valid, False otherwise.
27
+ """
28
+ video_path = (BASE_DIR / video_name).resolve()
29
+ return str(video_path).startswith(str(BASE_DIR))
30
+
31
+ def convert_to_hls(input_path, output_dir, video_name, start_number):
32
+ """
33
+ Converts an MP4 video to HLS format with 3-second segments starting from `start_number`.
34
+
35
+ Args:
36
+ input_path (str): Path to the input MP4 video.
37
+ output_dir (str): Directory where HLS files will be stored.
38
+ video_name (str): Name of the video (used for playlist naming).
39
+ start_number (int): The starting number for HLS segments.
40
+
41
+ Returns:
42
+ int: Number of segments generated.
43
+ """
44
+ # FFmpeg command to convert MP4 to HLS with 3-second segments
45
+ cmd = [
46
+ 'ffmpeg',
47
+ '-y', # Overwrite output files without asking
48
+ '-i', input_path, # Input file
49
+ #'-codec', 'copy', # Audio codec-vf scale=1280:720 -r 30 -c:v libx264 -b:v 1500k -c:a aac -b:a 128k
50
+ '-vf','scale=1280:720',
51
+ '-r','30',
52
+ '-c:v','libx264',
53
+ '-b:v','1500k',
54
+ '-c:a','aac',
55
+ '-b:a','128k',
56
+ '-hls_time', '1', # Segment duration in seconds
57
+ '-hls_playlist_type', 'vod', # Video on Demand playlist type
58
+ '-start_number', str(start_number), # Starting segment number
59
+ '-hls_segment_filename', os.path.join(output_dir, 'video_stream%d.ts'), # Segment file naming
60
+ os.path.join(output_dir, f'{video_name}.m3u8') # Output playlist
61
+ ]
62
+
63
+ try:
64
+ # Execute the FFmpeg command
65
+ subprocess.run(cmd, check=True, stderr=subprocess.PIPE)
66
+ except subprocess.CalledProcessError as e:
67
+ # Log FFmpeg errors and raise an HTTP exception
68
+ error_message = e.stderr.decode()
69
+ print(f"FFmpeg error: {error_message}")
70
+ raise HTTPException(status_code=500, detail="Video conversion failed")
71
+
72
+
73
+
74
+ async def add_video(video_name: str):
75
+ """
76
+ Endpoint to add a video to the streaming queue.
77
+
78
+ Args:
79
+ video_name (str): Name of the video file to add.
80
+ request (Request): FastAPI request object to extract base URL.
81
+
82
+ Returns:
83
+ dict: Success message.
84
+ """
85
+
86
+ print((HLS_DIR / f'{video_name}.m3u8').exists())
87
+ if not (HLS_DIR / f'{video_name}.m3u8').exists():
88
+ async with segment_lock:
89
+ global segment_counter
90
+ current_start_number = segment_counter
91
+ num_segments_before = len([f for f in os.listdir(HLS_DIR) if f.startswith('video_stream') and f.endswith('.ts')])
92
+ await asyncio.get_event_loop().run_in_executor(
93
+ None, convert_to_hls, str(video_name), str(HLS_DIR), video_name, current_start_number
94
+ )
95
+ num_segments_after = len([f for f in os.listdir(HLS_DIR) if f.startswith('video_stream') and f.endswith('.ts')])
96
+ num_new_segments = num_segments_after - num_segments_before
97
+ # Update the global segment counter
98
+ segment_counter += num_new_segments
99
+
100
+ video_names.append(video_name)
101
+ # Update concatenated playlist
102
+ #await concatenate_playlists(video_names, HLS_DIR)
103
+
104
+ return {"message": f'"{video_name}" added to the streaming queue.'}
105
+
106
+ async def concatenate_playlists(video_names, base_dir):
107
+ """
108
+ Concatenates multiple HLS playlists into a single playlist with unique segment numbering.
109
+
110
+ Args:
111
+ video_names (list): List of video names added to the queue.
112
+ base_dir (str): Base directory where HLS files are stored.
113
+ request (Request): FastAPI request object to extract base URL.
114
+ """
115
+ concatenated_playlist_path = os.path.join(base_dir, 'master.m3u8')
116
+ max_segment_duration = 1 # Since we set hls_time to 3
117
+ segment_lines = [] # To store segment lines
118
+
119
+ # Construct base URL from the incoming request
120
+
121
+
122
+ for video_name in video_names:
123
+ video_playlist_path = os.path.join(base_dir, f'{video_name}.m3u8')
124
+ if os.path.exists(video_playlist_path):
125
+ with open(video_playlist_path, 'r') as infile:
126
+ lines = infile.readlines()
127
+ for line in lines:
128
+ line = line.strip()
129
+ if line.startswith('#EXTINF'):
130
+ # Append EXTINF line
131
+ segment_lines.append(line)
132
+ elif line.endswith('.ts'):
133
+ segment_file = line
134
+ # Update segment URI to include full URL
135
+ segment_path = f'{segment_file}'
136
+ segment_lines.append(segment_path)
137
+ elif line.startswith('#EXT-X-BYTERANGE'):
138
+ # Include byte range if present
139
+ segment_lines.append(line)
140
+ elif line.startswith('#EXT-X-ENDLIST'):
141
+ # Do not include this here; we'll add it at the end
142
+ continue
143
+ 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'):
144
+ # Skip these tags; they'll be added in the concatenated playlist
145
+ continue
146
+ else:
147
+ # Include any other necessary tags
148
+ segment_lines.append(line)
149
+
150
+ # Write the concatenated playlist
151
+ with open(concatenated_playlist_path, 'w') as outfile:
152
+ outfile.write('#EXTM3U\n')
153
+ outfile.write('#EXT-X-PLAYLIST-TYPE:VOD\n')
154
+ outfile.write(f'#EXT-X-TARGETDURATION:{max_segment_duration}\n')
155
+ outfile.write('#EXT-X-VERSION:4\n')
156
+ outfile.write(f'#EXT-X-MEDIA-SEQUENCE:{1}\n') # Starting from segment number 1
157
+ for line in segment_lines:
158
+ outfile.write(f'{line}\n')
159
+ outfile.write('#EXT-X-ENDLIST\n')
160
+
161
+ def generate_m3u8(video_duration, output_path,segment_duration=1):
162
+ # Initialize playlist content
163
+ m3u8_content = "#EXTM3U\n"
164
+ m3u8_content += "#EXT-X-PLAYLIST-TYPE:VOD\n"
165
+ m3u8_content += f"#EXT-X-TARGETDURATION:{segment_duration}\n"
166
+ m3u8_content += "#EXT-X-VERSION:4\n"
167
+ m3u8_content += "#EXT-X-MEDIA-SEQUENCE:1\n"
168
+
169
+ # Calculate the number of full segments and the remaining duration
170
+
171
+ segment_duration = int(segment_duration)
172
+ full_segments = int(video_duration // segment_duration)
173
+ remaining_duration = video_duration % segment_duration
174
+
175
+ # Add full segments to the playlist
176
+ for i in range(full_segments):
177
+ m3u8_content += f"#EXTINF:{segment_duration:.6f},\n"
178
+ m3u8_content += f"/live_stream/video_stream{i + 1}.ts\n"
179
+
180
+ # Add the remaining segment if there's any leftover duration
181
+ if remaining_duration > 0:
182
+ m3u8_content += f"#EXTINF:{remaining_duration:.6f},\n"
183
+ m3u8_content += f"/live_stream/video_stream{full_segments + 1}.ts\n"
184
+
185
+ # End the playlist
186
+ m3u8_content += "#EXT-X-ENDLIST\n"
187
+ with open(output_path, "w") as file:
188
+ file.write(m3u8_content)
189
+
190
  print(f"M3U8 playlist saved to {output_path}")