|
|
import os |
|
|
import subprocess |
|
|
import pathlib |
|
|
|
|
|
import asyncio |
|
|
|
|
|
|
|
|
BASE_DIR = pathlib.Path('./videos').resolve() |
|
|
HLS_DIR = pathlib.Path('./hls_videos').resolve() |
|
|
HLS_DIR.mkdir(exist_ok=True) |
|
|
|
|
|
|
|
|
|
|
|
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_file, output_playlist, segment_prefix='segment', segment_duration=10): |
|
|
""" |
|
|
Converts an MP4 file to HLS .ts segments, maintaining continuity characteristics across segments. |
|
|
|
|
|
:param input_file: Path to the input MP4 file. |
|
|
:param output_playlist: Path to the output .m3u8 playlist file. |
|
|
:param segment_prefix: Prefix for naming the .ts segments. Default is 'segment'. |
|
|
:param segment_duration: Duration of each segment in seconds. Default is 10 seconds. |
|
|
""" |
|
|
if not os.path.exists(input_file): |
|
|
raise FileNotFoundError(f"Input file '{input_file}' does not exist.") |
|
|
|
|
|
|
|
|
os.chmod(input_file, 0o644) |
|
|
|
|
|
command = [ |
|
|
'ffmpeg', |
|
|
'-i', input_file, |
|
|
'-c:v', 'libx264', |
|
|
'-c:a', 'aac', |
|
|
'-strict', '-2', |
|
|
'-flags', '-global_header', |
|
|
'-hls_time', str(segment_duration), |
|
|
'-hls_list_size', '0', |
|
|
'-hls_flags', 'append_list+omit_endlist+program_date_time', |
|
|
'-hls_segment_type', 'mpegts', |
|
|
'-hls_segment_filename', f'{HLS_DIR}/{segment_prefix}%d.ts', |
|
|
'-force_key_frames', f'expr:gte(t,n_forced*{segment_duration})', |
|
|
'-avoid_negative_ts', 'make_zero', |
|
|
output_playlist |
|
|
] |
|
|
|
|
|
try: |
|
|
|
|
|
subprocess.run(command, check=True) |
|
|
print(f"Successfully converted '{input_file}' to HLS segments with playlist '{output_playlist}'.") |
|
|
except subprocess.CalledProcessError as e: |
|
|
print(f"Error during conversion: {e}") |
|
|
except Exception as e: |
|
|
print(f"An unexpected error occurred: {e}") |
|
|
|
|
|
|
|
|
|
|
|
def add_video(video_name, output_path, audio_duration): |
|
|
convert_to_hls(video_name, output_path, segment_duration=audio_duration) |
|
|
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 |
|
|
segment_lines = [] |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
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'): |
|
|
|
|
|
segment_lines.append(line) |
|
|
elif line.endswith('.ts'): |
|
|
segment_file = line |
|
|
|
|
|
segment_path = f'{segment_file}' |
|
|
segment_lines.append(segment_path) |
|
|
elif line.startswith('#EXT-X-BYTERANGE'): |
|
|
|
|
|
segment_lines.append(line) |
|
|
elif line.startswith('#EXT-X-ENDLIST'): |
|
|
|
|
|
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'): |
|
|
|
|
|
continue |
|
|
else: |
|
|
|
|
|
segment_lines.append(line) |
|
|
|
|
|
|
|
|
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') |
|
|
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): |
|
|
|
|
|
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" |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
with open(output_path, "w") as file: |
|
|
file.write(m3u8_content) |
|
|
|
|
|
print(f"M3U8 playlist saved to {output_path}") |