[音视频][HLS] HLS_downloader

news/2025/10/14 0:35:55/文章来源:https://www.cnblogs.com/migrator/p/19139798
[音视频][HLS] HLS_downloader

01 简介

1.1 功能:

一个简单的HLS下载器,使用go语言实现

1.2 执行方式

如果没有go执行环境,可以参照此处下载:GoLang DownLoad

运行方式为:将代码储存为:hls_downloader.go之后,执行以下命令:

go run hls_downloader.go [hls_url]

02 代码

package mainimport ("bufio""fmt""io""net/http""net/url""os""path""strings""sync""time"
)// 全局常量和变量
const (MaxConcurrentDownloads = 8               // 最大并发数DownloadInterval       = 5 * time.Second // 重试时间 sec
)var downloadedSegments = make(map[string]bool) // 去重
var downloadedMutex sync.Mutex                 // 互斥锁// 下载主函数
func loopDownloadHLS(m3u8URL, tempDir string) error {fmt.Printf("开始循环下载 HLS 流: %s\n", m3u8URL)fmt.Printf("媒体片段将保存到目录: %s\n", tempDir)// 创建临时目录if err := os.MkdirAll(tempDir, 0755); err != nil {return fmt.Errorf("创建保存目录失败: %w", err)}// 主循环:持续处理 M3U8 文件for {err := processM3U8(m3u8URL, tempDir)if err != nil {fmt.Printf("处理 M3U8 文件时发生错误: %v。将在 %v 后重试。\n", err, DownloadInterval)}time.Sleep(DownloadInterval)}
}// 处理 M3U8 的单次迭代
func processM3U8(m3u8URL, tempDir string) error {// 1. 下载主 M3U8 文件resp, err := http.Get(m3u8URL)if err != nil {return fmt.Errorf("下载 M3U8 文件失败: %w", err)}defer resp.Body.Close()if resp.StatusCode != http.StatusOK {return fmt.Errorf("M3U8 下载返回非 200 状态码: %d", resp.StatusCode)}// 2. 解析 M3U8 文件,获取 URL 列表urls, isMasterPlaylist, err := parseM3U8(resp.Body, m3u8URL)if err != nil {return fmt.Errorf("解析 M3U8 失败: %w", err)}if len(urls) == 0 {return fmt.Errorf("M3U8 中未找到任何链接")}// 处理主播放列表if isMasterPlaylist {// 选择主播放列表中的第一个文件进行设置selectedMediaURL := urls[0]fmt.Printf("[%s] 发现主播放列表,切换到媒体列表: %s\n", time.Now().Format("15:04:05"), selectedMediaURL)// 递归查询return processM3U8(selectedMediaURL, tempDir)}// 3. 筛选出新的、未下载过的 TS 文件newTSURLs := filterNewSegments(urls) // 此时 urls 已经保证是 tsURLsif len(newTSURLs) == 0 {fmt.Printf("[%s] 未发现新片段,等待下次检查...\n", time.Now().Format("15:04:05"))return nil}fmt.Printf("[%s] 发现 %d 个新片段,开始下载...\n", time.Now().Format("15:04:05"), len(newTSURLs))// 4. 并发下载所有新 TS 文件err = concurrentDownloadNew(newTSURLs, tempDir)if err != nil {return fmt.Errorf("并发下载新 TS 文件失败: %w", err)}return nil
}// 解析m3u8文件,返回URL列表和是否为主播放列表的标志。
// 如果是主列表,返回子 M3U8 链接列表;如果是媒体列表,返回媒体片段链接列表。
func parseM3U8(body io.Reader, baseURL string) ([]string, bool, error) {// 1. 读取整个 M3U8 内容到内存,以便进行标签搜索(类型识别)bodyBytes, err := io.ReadAll(body)if err != nil {return nil, false, fmt.Errorf("读取 M3U8 内容失败: %w", err)}content := string(bodyBytes)// 2. 通过 HLS 标签识别列表类型isMasterPlaylist := strings.Contains(content, "#EXT-X-STREAM-INF") // 主列表标志isMediaPlaylist := strings.Contains(content, "#EXTINF")            // 媒体列表标志// 3. 健壮性检查:HLS 规范不允许混用 Master 和 Media 标签if isMasterPlaylist && isMediaPlaylist {// 遇到混合列表,通常倾向于按媒体列表处理,并忽略 Master 标签fmt.Printf("警告: M3U8 文件同时包含 Master/Media 标签,按 Media 列表处理。\n")isMasterPlaylist = false} else if !isMasterPlaylist && !isMediaPlaylist {return nil, false, fmt.Errorf("M3U8 文件缺少 EXT-X-STREAM-INF 或 EXTINF 标签,无法识别列表类型")}// 4. 初始化 URL 解析器var urls []stringscanner := bufio.NewScanner(strings.NewReader(content))base, err := url.Parse(baseURL)if err != nil {return nil, false, fmt.Errorf("解析基础 URL 失败: %w", err)}// 5. 逐行扫描,提取所有非标签/非注释的 URLfor scanner.Scan() {line := strings.TrimSpace(scanner.Text())// 跳过注释行、标签行和空行if strings.HasPrefix(line, "#") || line == "" {continue}// 解析相对路径并合并到完整 URLu, err := base.Parse(line)if err != nil {fmt.Printf("警告: 无法解析 URL '%s': %v\n", line, err)continue}// 此时,非注释非标签的行,一定是 M3U8 链接或媒体片段链接urls = append(urls, u.String())}if err := scanner.Err(); err != nil {return nil, false, err}if len(urls) == 0 {return nil, false, fmt.Errorf("M3U8 文件中未找到任何有效链接")}// 6. 返回结果:urls 包含所有提取的链接,isMasterPlaylist 标志列表类型return urls, isMasterPlaylist, nil
}// 筛选出尚未下载的片段 URL
func filterNewSegments(tsURLs []string) []string {downloadedMutex.Lock()defer downloadedMutex.Unlock()var newURLs []stringfor _, u := range tsURLs {if !downloadedSegments[u] {newURLs = append(newURLs, u)// 立即标记为已发现(即使还未下载),避免下次检查时重复添加downloadedSegments[u] = true}}return newURLs
}// 并发下载新的 TS 文件
func concurrentDownloadNew(tsURLs []string, tempDir string) error {var wg sync.WaitGrouptsCount := len(tsURLs)sem := make(chan struct{}, MaxConcurrentDownloads) // 信号量限制并发数errChan := make(chan error, tsCount)               // 用于接收错误for i, tsURL := range tsURLs {wg.Add(1)sem <- struct{}{} // 占用一个信号量go func(index int, currentURL string) {defer wg.Done()defer func() { <-sem }() // 释放信号量// 解析 currentURLparsedURL, _ := url.Parse(currentURL)baseFilename := path.Base(parsedURL.Path)if !strings.HasSuffix(baseFilename, ".ts") {baseFilename += ".ts" // 确保后缀是 .ts}// 确保文件名唯一性,使用时间戳 + 序号作为前缀uniqueFilename := fmt.Sprintf("%s_%05d_%s", time.Now().Format("20060102_150405"), index, baseFilename)filename := path.Join(tempDir, uniqueFilename)err := downloadFile(currentURL, filename)if err != nil {// 将下载失败的片段从 '已下载' 列表中移除,以便下次重试downloadedMutex.Lock()delete(downloadedSegments, currentURL)downloadedMutex.Unlock()errChan <- fmt.Errorf("下载片段 %d (%s) 失败: %w", index, currentURL, err)return}// 打印进度fmt.Printf("  下载完成: %s\n", uniqueFilename)}(i, tsURL)}// 等待所有协程完成wg.Wait()close(errChan)// 检查是否有错误发生select {case err := <-errChan:return err // 只要有一个错误就返回default:return nil}
}// 下载单个文件(与前一个示例相同,包含简单重试)
func downloadFile(fileURL string, filepath string) error {const maxRetries = 3for i := 0; i < maxRetries; i++ {resp, err := http.Get(fileURL) // 修正了这里的参数名,使用 fileURLif err != nil {time.Sleep(time.Second * time.Duration(i+1))continue}defer resp.Body.Close()if resp.StatusCode != http.StatusOK {if i < maxRetries-1 {time.Sleep(time.Second * time.Duration(i+1))continue}return fmt.Errorf("HTTP 状态码错误: %d", resp.StatusCode)}// 如果文件已存在,则跳过下载,因为我们希望只下载一次if _, err := os.Stat(filepath); err == nil {return nil // 文件已存在,跳过下载}out, err := os.Create(filepath)if err != nil {return err}defer out.Close()_, err = io.Copy(out, resp.Body)return err}return fmt.Errorf("达到最大重试次数,下载失败: %s", fileURL)
}// 打印帮助文件
func printHelp() {appName := path.Base(os.Args[0]) // 获取程序本身的名称fmt.Printf("HLS 直播流下载器\n\n")fmt.Printf("使用方法:\n")fmt.Printf("  %s <M3U8_URL>\n\n", appName)fmt.Printf("参数:\n")fmt.Printf("  <M3U8_URL>  要下载的 HLS 播放列表 (M3U8) 的完整 URL。\n\n")fmt.Printf("示例:\n")fmt.Printf("  %s https://example.com/live/stream/playlist.m3u8\n", appName)
}func deriveOutputDir(hlsURL string) (string, error) {// 1. 解析 hlsURLparsedURL, err := url.Parse(hlsURL)if err != nil {return "", fmt.Errorf("解析 URL 失败: %w", err)}// 2. 获取路径中的文件名 (例如: xxxx.m3u8)filename := path.Base(parsedURL.Path)if filename == "" || filename == "." || filename == "/" {// 如果 URL 路径为空,或者只有斜杠,使用域名作为基础baseName := parsedURL.Hostif baseName == "" {return "default_hls_segments", nil}// 移除可能的端口号if colonIndex := strings.LastIndex(baseName, ":"); colonIndex != -1 {baseName = baseName[:colonIndex]}// 替换域名中的点号为下划线baseName = strings.ReplaceAll(baseName, ".", "_")return fmt.Sprintf("%s_hls_segments", baseName), nil}// 3. 移除 .m3u8 或 .M3u8 后缀baseName := filenameif strings.HasSuffix(baseName, ".m3u8") || strings.HasSuffix(baseName, ".M3u8") {// 移除最后 5 个字符 (.m3u8)baseName = baseName[:len(baseName)-5]}// 4. 清理文件名,确保目录名合法和安全// 将非字母数字、非下划线、非破折号的字符替换为下划线sanitizedBaseName := strings.Map(func(r rune) rune {if (r >= 'a' && r <= 'z') || (r >= 'A' && r <= 'Z') || (r >= '0' && r <= '9') || r == '-' || r == '_' {return r}return '_'}, baseName)// 5. 构建最终的 outputDir 名称outputDir := fmt.Sprintf("%s_hls_segments", sanitizedBaseName)return outputDir, nil
}func main() {var hlsURL string// 判断参数长度if len(os.Args) > 1 {hlsURL = os.Args[1]} else {printHelp()os.Exit(1)}// 创建输出目录outputDir, err := deriveOutputDir(hlsURL)if err != nil {fmt.Fprintf(os.Stderr, "错误: 无法确定下载目录: %v\n", err)os.Exit(1)}// 启动循环下载if err := loopDownloadHLS(hlsURL, outputDir); err != nil {fmt.Fprintf(os.Stderr, "下载器意外退出: %v\n", err)os.Exit(1)}
}

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mzph.cn/news/936543.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

Python-weakref技术指南

Python weakref 模块是 Python 标准库中用于处理对象弱引用的重要工具。它允许程序员创建对对象的弱引用,这种引用不会增加对象的引用计数,从而不影响对象的垃圾回收过程。本报告将全面介绍 weakref 模块的概念、工作…

从众多知识汲取一星半点也能受益匪浅【day11(2025.10.13)】

Enjoy 基于代码思考问题 先理清楚代码是否用上了文档所定义的api

王爽《汇编语言》第四章 笔记

4.2 源程序 4.2.1 伪指令在汇编语言的源程序中包含两种指令:汇编指令、伪指令。 (1)汇编指令:有对应机器码的指令,可以被编译为机器指令,最终被CPU所执行。 (2)伪指令:没有对应的机器指令,最终不被CPU所执行…

10.13总结

import java.util.*; import java.util.concurrent.TimeUnit; public class ArithmeticPractice { private Set generatedQuestions = new HashSet<>(); private List questions = new ArrayList<>(); pri…

MySql安装中的问题

是一台已经安装过但是失败了的win 1. 2025-10-13T12:42:20.566779Z 0 [ERROR] [MY-010457] [Server] --initialize specified but the data directory has files in it. Aborting. 2025-10-13T12:42:20.566788Z 0 [ERR…

10.14总结

import java.util.*; import java.util.concurrent.TimeUnit; public class ArithmeticPractice { private Set generatedQuestions = new HashSet<>(); private List questions = new ArrayList<>(); pri…

题解:AT_agc050_b [AGC050B] Three Coins

传送门 注:如无特殊说明,本篇题解中所有的序列,均用红色标示已经放置硬币的位置。若本次操作为拿走硬币,用蓝色标示本次操作拿走的硬币的位置,用黑色标示从未放过硬币或放置过硬币且在本次操作之前的操作中被拿走…

go:generate 指令

gogenerate 指令 go generate 命令是在Go语言 1.4 版本里面新添加的一个命令,当运行该命令时,它将扫描与当前包相关的源代码文件,找出所有包含 //go:generate 的特殊注释,提取并执行该特殊注释后面的命令。 命令格…

光栅化

光栅化 Rasterrization—光栅化(三角形的离散化) 屏幕(Screen)在图形学我们可以被抽象为一个二维数组,其中二维数组中的每个元素是像素( pixel )。 屏幕空间(screen space)是由数组构成的平面坐标系,每一个像…

图形学中的变换

图形学中的变换 二维变换 缩放变换(Scale)如上图,如果想把一个图形缩小为原来的0.5倍,那么就需要x坐标变为0.5倍,y坐标也变为0.5倍,可以用以下表达式表示这两个表达式可以用矩阵的形式表示如下Sx表示在x轴方向上…

Unity URP 体积云

Unity URP 体积云 ​ 好久之前开的体积云,因为期末考试和过年拖了很久,这几天才算整完。记录一样实现的思路,方便日后忘记了回来复习。 ​ 云的渲染有多种实现方法,我实现的是基于RayMarching的体积云体渲染,也…

使用DirectX绘制天空盒并实现破坏和放置方块

使用DirectX绘制天空盒并实现破坏和放置方块 绘制天空盒 由于项目中的DxTex软件使用不了,所以直接使用了方法二,将项目中的文件名直接修改,不过这里要注意获取的六个正方形贴图要用正确的顺序读取,也就是+X,-X,+…

编写DX12遇到的坑

编写DX12程序遇到的坑 ​ 写DX12每次遇到Bug都会卡好久,结果大部分时候最后都发现是一些小问题导致的,故将自己遇到的坑都写下来,方便后续遇到时回头查阅。 使用ClearDepthStencil清理DepthBuffer的时候把其他资源…

编写DX12时使用的辅助类

编写DX12时使用的辅助类 有一段时间没有学DX12,导致很多东西都忘了,跟着教程里写的东西还好,略看一遍教程就想起来的,但是自己封装的很多类就算写了注释过了一段时间也基本忘光,而且翻来翻去的也不方便,为了快速…

HLSL语法

语义 语义的概念语义xxxx:+ 大写单词,是用来限定输入值的来源、输出值的去向,其中那些大写单词都是系统提供的,我们需要用他们去填充我们的参数,然后传到顶点着色器和片元着色器中,进行进一步的计算,最后再通过…

DirectX12初始化

DirectX12初始化 这几天跟着龙书把dx12的初始化过了一遍,写点东西记一下,免得之后又忘了。 创建d3d设备 d3d设备相当于对显示适配器的抽象,显示适配器一般为显卡,也可由软件来模拟。可通过下列接口来创建一个d3d设…

用Vmware ESXI6.7离线包封装网卡驱动

用Vmware ESXI6.7离线包封装网卡驱动本来想装最新版的Vmware ESXI9.0的,但安装时提示找不到网卡无法安装,于是在网上搜索一番,发现可以用离线升级包封装网卡驱动的办法进行安装,但由于我的网卡是Realtek瑞昱RTL811…

CF2159B

Sol 假设 \(n<m\)。 考虑枚举列,然后对于每个位置分别做。 但是这非常难做,然后我们考虑包含 \([l,r]\) 这几行的最小矩形,然后发现这个东西可以在枚举列的时候同时计算,然后就做完了。 Code Link。

登录校验---Filter过滤器

过滤器(Filter)概念: Filter 过滤器,是 JavaWeb 三大组件(Servlet、Filter、Listener)之一。 过滤器可以把对资源的请求拦截下来,从而实现一些特殊的功能。 过滤器一般完成一些通用的操作,比如: 登录校验、统一编码…