ffmpeg
This commit is contained in:
95
main.go
95
main.go
@@ -5,6 +5,7 @@ import (
|
||||
"io"
|
||||
"net/http"
|
||||
"os"
|
||||
"os/exec"
|
||||
"strings"
|
||||
"sync"
|
||||
"time"
|
||||
@@ -19,16 +20,19 @@ type M3u8Res struct {
|
||||
}
|
||||
|
||||
// 创建一个连接池
|
||||
// http.Client 是 net/http 包里的核心结构,专门用来处理 HTTP 请求。它包含了一个底层的并发安全连接池
|
||||
var httpClient = &http.Client{
|
||||
Transport: &http.Transport{
|
||||
MaxIdleConnsPerHost: 5,
|
||||
MaxIdleConnsPerHost: 100, // 💡 修复1:调大连接池(设为100),避免高并发下被耗尽导致重新握手
|
||||
},
|
||||
}
|
||||
|
||||
// 获取输入的m3u8链接
|
||||
func getM3u8Path() (path string) {
|
||||
for {
|
||||
// fmt.Println 是格式化包 fmt 里的输出函数,会自动在末尾加换行
|
||||
fmt.Println("请输入m3u8链接地址:")
|
||||
// fmt.Scanln 等待你的终端输入,并把读取到的一行数据存入到 path 变量的内存地址(&path)中
|
||||
_, err := fmt.Scanln(&path)
|
||||
if err != nil {
|
||||
if err.Error() == "unexpected newline" {
|
||||
@@ -44,16 +48,19 @@ func getM3u8Path() (path string) {
|
||||
|
||||
// Get链接
|
||||
func fetchM3u8Content(path string) string {
|
||||
// 发起一个真正的 HTTP GET 网络请求。
|
||||
res, err := httpClient.Get(path)
|
||||
if err != nil {
|
||||
fmt.Println(err)
|
||||
return ""
|
||||
}
|
||||
// defer 用于注册延迟调用,确保这个函数无论怎样返回,都会在最后一步关闭连接释放网络资源,防止内存漏洞
|
||||
defer res.Body.Close()
|
||||
if res.StatusCode != 200 {
|
||||
fmt.Println("出了点问题", res.StatusCode)
|
||||
return ""
|
||||
}
|
||||
// io.ReadAll 是标准库 io(输入输出) 里的工具。它可以把网络流数据 (res.Body) 一次性全部读取到内存字节流
|
||||
body, err := io.ReadAll(res.Body)
|
||||
if err != nil {
|
||||
fmt.Println(err)
|
||||
@@ -63,9 +70,11 @@ func fetchM3u8Content(path string) string {
|
||||
}
|
||||
|
||||
func parseM3u8(content string, baseUrl string) M3u8Res {
|
||||
// strings.Split 按照换行符 \n 把长长的文本打散成一个字符串数组,也就是一行一行的
|
||||
lines := strings.Split(content, "\n")
|
||||
// 优先级最高:检查是否包含 Master 标签
|
||||
for i, line := range lines {
|
||||
// strings.Contains 用于检索 line 这句话里面是否包含了某个特定字符串
|
||||
if strings.Contains(line, "#EXT-X-STREAM-INF") {
|
||||
return parseMainContent(lines, i, baseUrl)
|
||||
}
|
||||
@@ -79,6 +88,7 @@ func parseM3u8(content string, baseUrl string) M3u8Res {
|
||||
func parseMainContent(lines []string, index int, baseUrl string) M3u8Res {
|
||||
// 取匹配标签的下一行作为 URL
|
||||
if index+1 < len(lines) {
|
||||
// strings.TrimSpace 是去除字符串开头和结尾的空白字符(比如多余的空格或者换行回车符\r)
|
||||
nextUrl := strings.TrimSpace(lines[index+1])
|
||||
return M3u8Res{
|
||||
IsMaster: true,
|
||||
@@ -91,10 +101,13 @@ func parseMainContent(lines []string, index int, baseUrl string) M3u8Res {
|
||||
func parseSubContent(lines []string) []string {
|
||||
var tsFiles []string
|
||||
for _, line := range lines {
|
||||
// 先去除两侧的空格回车
|
||||
line = strings.TrimSpace(line)
|
||||
// strings.HasPrefix 判断字符串 line 是不是以 "#" 号开头
|
||||
if strings.HasPrefix(line, "#") || line == "" {
|
||||
continue
|
||||
}
|
||||
// strings.HasSuffix 判断字符串的结尾,检查是不是这两种结尾形式
|
||||
if strings.HasSuffix(line, ".jpeg") || strings.HasSuffix(line, ".ts") {
|
||||
tsFiles = append(tsFiles, line)
|
||||
}
|
||||
@@ -104,6 +117,7 @@ func parseSubContent(lines []string) []string {
|
||||
|
||||
// 提取基础路径
|
||||
func getBaseUrl(path string) string {
|
||||
// strings.LastIndex 返回 "/" 在这个网址里最后一次出现的位置索引下标(数字)
|
||||
lastSlash := strings.LastIndex(path, "/")
|
||||
if lastSlash == -1 {
|
||||
return path + "/"
|
||||
@@ -132,13 +146,17 @@ func buildUrl(baseUrl string, m3u8TsFile []string) []string {
|
||||
}
|
||||
|
||||
// 协程用 index int 是用来标识每个文件的序号,在并发下载中特别重要
|
||||
func downLoadOne(Url string, index int) error {
|
||||
func downLoadOne(Url string, index int, tempDir string) error {
|
||||
maxRetry := 3 // 定义最大重试次数
|
||||
// time.Second 是 time 库里的标准时长常量代表 1 秒钟
|
||||
baseDelay := time.Second
|
||||
for retry := 0; retry < maxRetry; retry++ {
|
||||
if retry > 0 {
|
||||
// time.Duration 是时间间隔类型,(1<<uint(retry-1)) 是位运算乘法。代表指数退避等待时间 (1s, 2s, 4s)
|
||||
waitTime := baseDelay * time.Duration(1<<uint(retry-1)) // 1s, 2s, 4s
|
||||
// fmt.Printf 是带占位符的格式化打印函数,%d等是占位符,用来将后面的变量安插进去打印
|
||||
fmt.Printf("[%3d] 第 %d 次重试,等待 %v...\n", index, retry, waitTime)
|
||||
// time.Sleep 会让执行到此的协程挂起(沉睡)指定的时间,以留出时间重连并不给服务器造成大负担
|
||||
time.Sleep(waitTime)
|
||||
}
|
||||
resp, err := httpClient.Get(Url)
|
||||
@@ -156,17 +174,21 @@ func downLoadOne(Url string, index int) error {
|
||||
}
|
||||
return fmt.Errorf("服务器状态异常: %d", resp.StatusCode)
|
||||
}
|
||||
fileName := fmt.Sprintf("%d", index)
|
||||
// fmt.Sprintf 不直接抛出屏幕,而是把变量合并并返回字符串表示,我们将它当作文件名
|
||||
fileName := fmt.Sprintf("%s/%d.ts", tempDir, index) // 💡 修复2:存入临时目录,同时加上 .ts 后缀
|
||||
// os.Create 在硬盘上对应路径下创建一个新文件,如果存在同名文件则会被从头清空截断为0!
|
||||
file, err := os.Create(fileName)
|
||||
if err != nil {
|
||||
resp.Body.Close()
|
||||
return err
|
||||
}
|
||||
|
||||
// io.Copy 核心函数:它可以源源不断地把 resp.Body (网络传输过来的字节流) 搬运到 file (刚才创的空文件) 中,极其省内存,不卡电脑
|
||||
_, err = io.Copy(file, resp.Body)
|
||||
closeErr := file.Close()
|
||||
resp.Body.Close()
|
||||
if err != nil || closeErr != nil {
|
||||
// os.Remove 直接让操作系统把这个产生坏结果的文件删掉清理掉
|
||||
os.Remove(fileName)
|
||||
if err != nil {
|
||||
if retry < maxRetry-1 {
|
||||
@@ -182,21 +204,28 @@ func downLoadOne(Url string, index int) error {
|
||||
}
|
||||
|
||||
// 并行下载函数
|
||||
func downLoadALL(fullUrl []string, maxConcurrent int) {
|
||||
func downLoadALL(fullUrl []string, maxConcurrent int, tempDir string) {
|
||||
// sync.WaitGroup 是极经典的同步原语。本质上是个高级计数器,用于等待一群分派出去的并发任务全军凯旋
|
||||
var wait sync.WaitGroup
|
||||
total := len(fullUrl)
|
||||
bar := progressbar.Default(int64(total), "正在下载(%d)")
|
||||
// make 函数专门用于创建 channel、map 或 slice,这里构建了一个带最大缓冲容量的消息信道
|
||||
maxT := make(chan struct{}, maxConcurrent) //定义一个长度为maxConcurrent的信道
|
||||
// wait.Add 把总任务数一次性全加进计数器里
|
||||
wait.Add(len(fullUrl))
|
||||
for i := range fullUrl {
|
||||
// 当信道满载 10 个时,这行代码会被迫卡住(阻塞)等别人拿出来。达到限制峰值避免超额的并发控制
|
||||
maxT <- struct{}{} // 写入一个信号
|
||||
// 关键字 go,代表直接分出一个独立的执行线(协程,极低开销)去运行内部代码块
|
||||
go func(url string, index int) {
|
||||
defer func() {
|
||||
// <-maxT 事情干完或出错了,从信道抽走一个信号释放位置,这会让刚才卡住的地方马上填装信道创建下一个兵
|
||||
<-maxT
|
||||
bar.Add(1)
|
||||
}()
|
||||
// wait.Done 给总计任务器数量减一。表示单个子业务已经闭环
|
||||
defer wait.Done()
|
||||
err := downLoadOne(url, index)
|
||||
err := downLoadOne(url, index, tempDir)
|
||||
if err != nil {
|
||||
fmt.Printf("文件 %d 失败: %v ", index, err)
|
||||
}
|
||||
@@ -207,28 +236,33 @@ func downLoadALL(fullUrl []string, maxConcurrent int) {
|
||||
}
|
||||
|
||||
// 碎片拼接
|
||||
func videoBuild(num int, outputpath string, deleteAfter bool) error {
|
||||
output, err := os.Create(outputpath)
|
||||
func videoBuild(num int, outputpath string, deleteAfter bool, tempDir string) error {
|
||||
// 💡 修复3:通过相对路径调用 ffmpeg 混流
|
||||
listFileName := fmt.Sprintf("%s/mylist.txt", tempDir)
|
||||
listFile, err := os.Create(listFileName)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer output.Close()
|
||||
|
||||
for i := 0; i < num; i++ {
|
||||
fileName := fmt.Sprintf("%d", i)
|
||||
input, err := os.Open(fileName)
|
||||
if err != nil {
|
||||
return fmt.Errorf("打开文件失败\n")
|
||||
}
|
||||
writtenBytes, err := io.Copy(output, input)
|
||||
input.Close()
|
||||
if err != nil {
|
||||
return fmt.Errorf("合并文件失败\n")
|
||||
}
|
||||
if deleteAfter {
|
||||
os.Remove(fileName)
|
||||
}
|
||||
fmt.Printf("合并成功,%d\n", writtenBytes)
|
||||
listFile.WriteString(fmt.Sprintf("file '%d.ts'\n", i))
|
||||
}
|
||||
listFile.Close()
|
||||
|
||||
ffmpegPath := "./ffmpeg/ffmpeg.exe" // 如果是 Mac/Linux,把 .exe 去掉
|
||||
// 组装合并命令
|
||||
cmd := exec.Command(ffmpegPath, "-f", "concat", "-safe", "0", "-i", listFileName, "-c", "copy", outputpath)
|
||||
|
||||
fmt.Println("\n正在调用 FFmpeg 进行无损合并...")
|
||||
err = cmd.Run()
|
||||
if err != nil {
|
||||
return fmt.Errorf("FFmpeg 混流失败 (请确认当前目录下放入了 ffmpeg/ffmpeg.exe): %v\n", err)
|
||||
}
|
||||
|
||||
fmt.Println("🎉 FFmpeg 处理完成!")
|
||||
|
||||
if deleteAfter {
|
||||
os.RemoveAll(tempDir) // 直接清空整个临时目录(包含所有 .ts 和 mylist.txt)
|
||||
}
|
||||
|
||||
return nil
|
||||
@@ -239,21 +273,26 @@ func main() {
|
||||
m3u8Body := fetchM3u8Content(m3u8Path) // get到链接文件
|
||||
m3u8BaseUrl := getBaseUrl(m3u8Path) // 获取基础路径
|
||||
m3u8TsFile := parseM3u8(m3u8Body, m3u8BaseUrl)
|
||||
|
||||
// 💡 修复2:创建临时存文件的目录
|
||||
tempDir := "./temp_download"
|
||||
os.MkdirAll(tempDir, os.ModePerm)
|
||||
|
||||
if m3u8TsFile.IsMaster {
|
||||
fmt.Println("检测到主播放列表,准备跳转:", m3u8TsFile.NextUrl)
|
||||
baseNew := getBaseUrl(m3u8TsFile.NextUrl) // 新的基础路径
|
||||
m3u8Body = fetchM3u8Content(m3u8TsFile.NextUrl) // get新的内容
|
||||
m3u8TsFileNew := parseM3u8(m3u8Body, baseNew)
|
||||
fullUrl := buildUrl(baseNew, m3u8TsFileNew.TsFile)
|
||||
downLoadALL(fullUrl, 10)
|
||||
fmt.Printf("下载完成?开始合并...\n")
|
||||
videoBuild(len(m3u8TsFileNew.TsFile), "test.mp4", true)
|
||||
downLoadALL(fullUrl, 10, tempDir)
|
||||
fmt.Printf("\n下载完成?开始合并...\n")
|
||||
videoBuild(len(m3u8TsFileNew.TsFile), "test.mp4", true, tempDir)
|
||||
} else {
|
||||
fmt.Println("获得分片列表,准备下载:", len(m3u8TsFile.TsFile), "个文件")
|
||||
fullUrl := buildUrl(m3u8BaseUrl, m3u8TsFile.TsFile)
|
||||
downLoadALL(fullUrl, 10)
|
||||
fmt.Printf("下载完成?开始合并...\n")
|
||||
videoBuild(len(m3u8TsFile.TsFile), "test.mp4", true)
|
||||
downLoadALL(fullUrl, 10, tempDir)
|
||||
fmt.Printf("\n下载完成?开始合并...\n")
|
||||
videoBuild(len(m3u8TsFile.TsFile), "test.mp4", true, tempDir)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user