如何根据起始与结束Unix时间从M3U8 HLS播放列表提取片段?
从HLS M3U8播放列表中按Unix时间范围提取片段的实现方案
Python 实现
思路
- 解析M3U8文件时保留核心头部标签(如
#EXTM3U、#EXT-X-VERSION等),这些是播放列表的必要结构。 - 针对每个媒体片段,通过
#EXT-X-PROGRAM-DATE-TIME标签获取起始ISO时间,转换为毫秒级Unix时间戳;通过#EXTINF标签获取片段时长,计算出片段结束时间(起始时间 + 时长*1000)。 - 对比片段时间范围与目标起始/结束时间,筛选出所有与目标范围有交集的片段,连同对应标签一起保留。
- 输出由符合条件的片段组成的新M3U8文件,确保播放兼容性。
代码实现
import datetime def parse_iso_to_unix_ms(iso_str): # 将ISO8601时间字符串转换为毫秒级Unix时间戳 dt = datetime.datetime.fromisoformat(iso_str.replace('Z', '+00:00')) return int(dt.timestamp() * 1000) def extract_m3u8_segment_by_time(input_m3u8, output_m3u8, start_ms, end_ms): with open(input_m3u8, 'r', encoding='utf-8') as f_in, open(output_m3u8, 'w', encoding='utf-8') as f_out: current_segment_start = None current_segment_duration = None is_in_segment = False # 遍历处理每一行 for line in f_in: line = line.strip() if not line: continue # 写入核心头部标签 if line.startswith('#EXTM3U') or line.startswith('#EXT-X-VERSION') or line.startswith('#EXT-X-TARGETDURATION'): f_out.write(line + '\n') continue # 解析片段起始时间 if line.startswith('#EXT-X-PROGRAM-DATE-TIME:'): iso_time = line.split(':', 1)[1] current_segment_start = parse_iso_to_unix_ms(iso_time) continue # 解析片段时长 if line.startswith('#EXTINF:'): duration_str = line.split(',')[0].split(':')[1] current_segment_duration = float(duration_str) is_in_segment = True continue # 处理媒体片段URL if is_in_segment and current_segment_start is not None: segment_end = current_segment_start + int(current_segment_duration * 1000) # 筛选与目标时间范围有交集的片段 if not (segment_end < start_ms or current_segment_start > end_ms): f_out.write(f'#EXTINF:{current_segment_duration},\n') f_out.write(f'#EXT-X-PROGRAM-DATE-TIME:{datetime.datetime.fromtimestamp(current_segment_start/1000, datetime.timezone.utc).isoformat().replace("+00:00", "Z")}\n') f_out.write(line + '\n') is_in_segment = False current_segment_start = None current_segment_duration = None # 写入播放列表结束标签 f_out.write('#EXT-X-ENDLIST\n') # 使用示例 if __name__ == '__main__': INPUT_FILE = 'input.m3u8' OUTPUT_FILE = 'output.m3u8' START_MS = 1667112116900 END_MS = 1667112133040 extract_m3u8_segment_by_time(INPUT_FILE, OUTPUT_FILE, START_MS, END_MS)
关键说明
parse_iso_to_unix_ms函数完成ISO时间到毫秒级Unix时间戳的转换,匹配用户提供的时间格式。- 筛选逻辑采用交集判断:只要片段结束时间不早于目标起始时间,且片段起始时间不晚于目标结束时间,就保留该片段。
- 输出的M3U8保留了必要的结构标签,确保能被标准HLS播放器识别。
C++ 实现
思路
- 逐行读取M3U8文件,识别并保留核心头部标签。
- 解析
EXT-X-PROGRAM-DATE-TIME的ISO8601时间字符串,转换为毫秒级Unix时间戳(依赖C++20的<chrono>和<date>库简化时间处理)。 - 解析
EXTINF的时长信息,计算每个片段的结束时间。 - 对比片段时间范围与目标时间,筛选符合条件的片段并写入新文件。
代码实现
#include <iostream> #include <fstream> #include <string> #include <chrono> #include <date/date.h> #include <algorithm> #include <sstream> using namespace std; using namespace date; using namespace chrono; // 将ISO8601时间字符串转换为毫秒级Unix时间戳 int64_t parse_iso_to_unix_ms(const string& iso_str) { sys_seconds tp; istringstream ss(iso_str); ss >> parse("%Y-%m-%dT%H:%M:%SZ", tp); return duration_cast<milliseconds>(tp.time_since_epoch()).count(); } // 将毫秒级Unix时间戳转换为ISO8601字符串 string unix_ms_to_iso(int64_t ms) { sys_time<milliseconds> tp{milliseconds{ms}}; return format("%Y-%m-%dT%H:%M:%SZ", tp); } // 去除字符串首尾空格 string trim(const string& s) { auto start = s.begin(); while (start != s.end() && isspace(*start)) { start++; } auto end = s.end(); do { end--; } while (distance(start, end) > 0 && isspace(*end)); return string(start, end + 1); } void extract_m3u8_segment_by_time(const string& input_path, const string& output_path, int64_t start_ms, int64_t end_ms) { ifstream in_file(input_path); ofstream out_file(output_path); if (!in_file.is_open() || !out_file.is_open()) { cerr << "Failed to open file" << endl; return; } string line; int64_t current_start_ms = -1; double current_duration = 0.0; bool in_segment = false; while (getline(in_file, line)) { string trimmed_line = trim(line); if (trimmed_line.empty()) continue; // 写入核心头部标签 if (trimmed_line.substr(0, 7) == "#EXTM3U" || trimmed_line.substr(0, 15) == "#EXT-X-VERSION" || trimmed_line.substr(0, 22) == "#EXT-X-TARGETDURATION") { out_file << line << endl; continue; } // 解析片段起始时间 if (trimmed_line.substr(0, 24) == "#EXT-X-PROGRAM-DATE-TIME:") { string iso_time = trimmed_line.substr(24); current_start_ms = parse_iso_to_unix_ms(iso_time); continue; } // 解析片段时长 if (trimmed_line.substr(0, 8) == "#EXTINF:") { size_t comma_pos = trimmed_line.find(','); if (comma_pos != string::npos) { string duration_str = trimmed_line.substr(8, comma_pos - 8); current_duration = stod(duration_str); in_segment = true; } continue; } // 处理媒体片段URL if (in_segment && current_start_ms != -1) { int64_t segment_end_ms = current_start_ms + static_cast<int64_t>(current_duration * 1000); // 筛选与目标时间范围有交集的片段 if (!(segment_end_ms < start_ms || current_start_ms > end_ms)) { out_file << "#EXTINF:" << current_duration << ",\n"; out_file << "#EXT-X-PROGRAM-DATE-TIME:" << unix_ms_to_iso(current_start_ms) << "\n"; out_file << line << "\n"; } in_segment = false; current_start_ms = -1; current_duration = 0.0; } } // 写入播放列表结束标签 out_file << "#EXT-X-ENDLIST\n"; in_file.close(); out_file.close(); } int main() { string input_file = "input.m3u8"; string output_file = "output.m3u8"; int64_t start_ms = 1667112116900; int64_t end_ms = 1667112133040; extract_m3u8_segment_by_time(input_file, output_file, start_ms, end_ms); return 0; }
关键说明
- 代码依赖C20的
<date>库,该库已被纳入部分主流编译器的C20标准实现中;若使用的编译器未支持,可手动引入对应的头文件实现时间解析功能。 - 时间交集判断逻辑与Python版本完全一致,确保跨语言筛选结果一致。
- 处理文件时保留原始行格式(仅去除首尾空格),避免破坏M3U8的兼容性。
内容的提问来源于stack exchange,提问作者AanisNoor
相关产品推荐
相关产品推荐

