Spaces:
Paused
Paused
File size: 9,251 Bytes
c93c8fd eddd7de c93c8fd 3de7ae7 c93c8fd 581abfa c93c8fd eddd7de c93c8fd 581abfa febda44 6775f15 581abfa 6775f15 581abfa cf036a0 581abfa febda44 581abfa cf036a0 c93c8fd ffa0ee3 cf036a0 7818ba9 eddd7de 7818ba9 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 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 |
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
total_processed_duration = 0
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, output_path, audio_duration):
"""
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.
"""
global total_processed_duration
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)
segment_duration = 3
full_segments = int(audio_duration // segment_duration)
remaining_duration = audio_duration % segment_duration
existing_segments = set()
if output_path:
with open(output_path, 'r') as m3u8_file:
for line in m3u8_file:
if line.startswith('/live_stream/video_stream'):
existing_segments.add(line.strip())
new_segments = []
# Generate new segment paths
for i in range(1, full_segments + 1):
new_segment_path = f"/live_stream/video_stream{i}.ts"
if new_segment_path not in existing_segments:
new_segments.append(new_segment_path)
total_processed_duration += segment_duration
with open(output_path, 'a') as m3u8_file:
for segment in new_segments:
m3u8_file.write(f"#EXTINF:{segment_duration:.3f},\n")
m3u8_file.write(f"{segment}\n")
if remaining_duration > 0:
remaining_segment_path = f"/live_stream/video_stream{full_segments + 1}.ts"
if remaining_segment_path not in existing_segments:
m3u8_file.write(f"#EXTINF:{remaining_duration:.3f},\n")
m3u8_file.write(f"{remaining_segment_path}\n")
total_processed_duration += remaining_duration
# Add #EXT-X-ENDLIST only once after all segments have been added
if total_processed_duration == video_duration:
with open(output_path, 'a') as m3u8_file:
m3u8_file.write("#EXT-X-ENDLIST\n")
total_processed_duration = 0
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"
# # 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}") |