Spaces:
Paused
Paused
Update stream_server.py
Browse files- 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', '
|
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 =
|
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=
|
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}")
|