/* * @Author: BlackTeay * @Date: 2024-04-30 09:37:39 * @LastEditTime: 2024-05-08 14:43:47 * @LastEditors: BlackTeay * @Description: * @FilePath: /hls_builder/main.go * Copyright 2024 JLNTV NMTD, All Rights Reserved. */ package main import ( "bufio" "embed" _ "embed" "errors" "fmt" "image/color" "log" "math" "net/url" "os" "os/exec" "path/filepath" "regexp" "runtime" "strconv" "time" "fyne.io/fyne/v2" "fyne.io/fyne/v2/app" "fyne.io/fyne/v2/container" "fyne.io/fyne/v2/dialog" "fyne.io/fyne/v2/layout" "fyne.io/fyne/v2/storage" "fyne.io/fyne/v2/theme" "fyne.io/fyne/v2/widget" "github.com/google/uuid" ) //go:embed "font/NotoSansSC-Regular.ttf" var ttfBytes []byte //go:embed bin/* var ffmpegFS embed.FS // MyTheme 定义了一个自定义的主题结构体。 type MyTheme struct{} // Color 根据给定的主题颜色名称和变种返回相应的颜色。 // 参数 c 为主题颜色的名称。 // 参数 v 为颜色的变种。 // 返回值为符合 color.Color 接口的颜色值。 func (MyTheme) Color(c fyne.ThemeColorName, v fyne.ThemeVariant) color.Color { return theme.DefaultTheme().Color(c, v) } // Font 根据给定的文本样式返回相应的字体资源。 // 参数 s 为文本的样式,决定了使用的字体。 // 返回值为一个 fyne.Resource 类型的字体资源。 func (MyTheme) Font(s fyne.TextStyle) fyne.Resource { return fyne.NewStaticResource("./font/NotoSansSC-Regular.ttf", ttfBytes) } // Icon 根据给定的图标名称返回相应的图标资源。 // 参数 n 为图标的名称。 // 返回值为一个 fyne.Resource 类型的图标资源。 func (MyTheme) Icon(n fyne.ThemeIconName) fyne.Resource { return theme.DefaultTheme().Icon(n) } // Size 根据给定的主题尺寸名称返回相应的尺寸值。 // 参数 s 为主题尺寸的名称。 // 返回值为相应的尺寸值,单位为 float32。 func (MyTheme) Size(s fyne.ThemeSizeName) float32 { return theme.DefaultTheme().Size(s) } type UploadStats struct { Successed int Failed int TotalSize string AverageSpeed string Elapsed string } // getFFmpegPath 获取FFmpeg可执行文件的路径。 // 该函数根据运行时操作系统选择对应的FFmpeg文件,将该文件从嵌入的文件系统读出, // 并写入到一个临时目录中,然后返回这个临时文件的路径。 // 返回值: // // string - FFmpeg可执行文件的临时路径。 func getFFmpegPath() (string, func(), error) { var ffmpegFileName string switch runtime.GOOS { case "windows": ffmpegFileName = "ffmpeg_windows.exe" case "darwin": ffmpegFileName = "ffmpeg_darwin" } // 根据操作系统选择的FFmpeg文件名,从嵌入的文件系统中读取文件内容 data, err := ffmpegFS.ReadFile("bin/" + ffmpegFileName) if err != nil { log.Fatal(err) } // 创建临时目录并将FFmpeg文件内容写入该目录中的文件 tmpDir, err := os.MkdirTemp("", "ffmpeg") if err != nil { log.Fatal(err) } tmpFilePath := filepath.Join(tmpDir, ffmpegFileName) if err := os.WriteFile(tmpFilePath, data, 0755); err != nil { log.Fatal(err) } else { log.Println("FFmpeg path:", tmpFilePath) } cleanup := func() { os.RemoveAll(tmpDir) // 删除整个临时目录 log.Println("Cleaned up FFmpeg temporary files.") } return tmpFilePath, cleanup, nil } // getUs3cliPath 获取us3cli二进制文件的临时路径。 // 该函数根据运行的操作系统选择对应的us3cli文件,并将其从嵌入的文件系统中读出, // 然后写入到一个临时文件中,最后返回这个临时文件的路径。 // 返回值: // - string: us3cli临时文件的路径。 func getUs3cliPath() (string, func(), error) { var us3cliFileName string switch runtime.GOOS { case "windows": us3cliFileName = "us3cli-windows.exe" case "darwin": us3cliFileName = "us3cli-mac" } // 根据操作系统选择的us3cli文件名,从嵌入的文件系统中读取文件内容 data, err := ffmpegFS.ReadFile("bin/" + us3cliFileName) if err != nil { log.Fatal(err) } // 创建临时目录并将us3cli文件内容写入其中 tmpDir, err := os.MkdirTemp("", "us3cli") if err != nil { log.Fatal(err) } tmpFilePath := filepath.Join(tmpDir, us3cliFileName) if err := os.WriteFile(tmpFilePath, data, 0755); err != nil { log.Fatal(err) } else { log.Println("us3cli path:", tmpFilePath) } cleanup := func() { os.RemoveAll(tmpDir) // 删除整个临时目录 log.Println("Cleaned up Us3cli temporary files.") } return tmpFilePath, cleanup, nil } // main 是应用程序的入口点。 func main() { // 打印当前工作目录 log.Println("Current path:", os.Getenv("PWD")) // 初始化应用并设置主题 myApp := app.NewWithID("hls_builder") myApp.Settings().SetTheme(MyTheme{}) // 创建主窗口并设置大小 myWindow := myApp.NewWindow("HLS-builder 视频切片上传工具") myWindow.Resize(fyne.NewSize(800, 400)) // 初始化UI组件:文件选择提示、输出路径选择提示、进度条 inputFile := widget.NewLabel("请选择mp4文件!") outputDir := widget.NewLabel("请选择输出路径!") sliceProgressBar := widget.NewProgressBar() uploadProgressBar := widget.NewProgressBar() // 创建选择视频文件按钮并设置点击事件 btnSelectFile := widget.NewButton("选择mp4文件", func() { // 设置文件类型过滤器 filter := storage.NewExtensionFileFilter([]string{".mp4"}) fileDialog := dialog.NewFileOpen(func(file fyne.URIReadCloser, err error) { if err != nil { log.Println("Error opening file:", err) return } if file != nil { log.Println("Opened file:", file.URI().Path()) inputFile.SetText(file.URI().Path()) } }, myWindow) fileDialog.SetFilter(filter) fileDialog.Show() }) // 创建选择输出路径按钮并设置点击事件 btnSelectOutput := widget.NewButton("选择输出路径", func() { dialog.ShowFolderOpen(func(uri fyne.ListableURI, err error) { if err == nil && uri != nil { log.Println("Selected folder:", uri.Path()) outputDir.SetText(uri.Path()) } }, myWindow) }) var randDir string var hlsDir string // 初始化上传按钮 uploadLabel := widget.NewLabel("上传切片到Ucloud:") var btnUpload *widget.Button btnUpload = widget.NewButton("上传", func() { btnUpload.Text = "正在上传..." btnUpload.Disable() // 调用上传函数,并在上传完成后启用按钮 go upload(hlsDir, randDir, uploadProgressBar, myWindow, func() { btnUpload.Text = "上传" btnUpload.Enable() }) }) // 初始化切片按钮及相关变量 var btnSlice *widget.Button btnSlice = widget.NewButton("开始切片", func() { fmt.Println(inputFile.Text) fmt.Println(outputDir.Text) // 验证用户选择 if inputFile.Text == "请选择mp4文件!" || outputDir.Text == "请选择输出路径!" { dialog.ShowError(errors.New("请选择视频文件和输出路径"), myWindow) return } // 禁用UI元素以防止用户干预 btnSelectFile.Disable() btnSelectOutput.Disable() btnSlice.Disable() btnSlice.SetText("切片中...") // 生成随机目录名并在输出路径下创建该目录 randDir = uuid.New().String() hlsDir = outputDir.Text + "/" + randDir err := os.Mkdir(hlsDir, 0755) if err != nil { dialog.ShowError(errors.New("建立随机目录出错!"), myWindow) } // 调用切片函数,并在切片完成后更新UI go sliceVideo(inputFile.Text, hlsDir, sliceProgressBar, func() { btnSelectFile.Enable() btnSelectOutput.Enable() btnSlice.Enable() btnSlice.SetText("开始切片") }, btnSelectFile, btnSelectOutput, btnSlice, btnUpload, myWindow) }) btnUpload.Disable() copyRight := widget.NewLabelWithStyle("© 2024 JLNTV ", fyne.TextAlignCenter, fyne.TextStyle{Bold: true}) blogLink := widget.NewHyperlink("BlackTeay", createURL("https://iteay.top")) copyRightHyperlink := container.NewHBox(copyRight, blogLink) // 创建一个水平和垂直居中的布局 centeredLayout := container.NewBorder(nil, nil, nil, nil, copyRightHyperlink) // 组合UI元素 content := container.NewVBox( inputFile, btnSelectFile, outputDir, btnSelectOutput, sliceProgressBar, btnSlice, uploadLabel, btnUpload, uploadProgressBar, ) // 将内容放置在水平和垂直居中的布局中 centeredContent := container.New(layout.NewVBoxLayout(), content, centeredLayout) // 设置窗口内容并显示 myWindow.SetContent(centeredContent) myWindow.ShowAndRun() } // sliceVideo 切割视频文件为HLS格式的多个段。 // // 参数: // inputFile - 输入视频文件的路径。 // outputDir - 输出目录的路径,切片和播放列表将保存于此目录。 // progressBar - 用于显示切割过程进度的进度条。 // onComplete - 切割完成后调用的回调函数。 // btnSelectFile, btnSelectOutput, btnSlice - 用于用户界面交互的按钮。 // myWindow - 所属的FYNE窗口,用于显示对话框和其他UI元素。 // // 此函数使用FFmpeg工具切割视频,将视频文件分解为多个时长为5秒的HLS段,并创建一个播放列表。 // 过程中会更新进度条,并在完成时显示一个通知对话框。 func sliceVideo(inputFile, outputDir string, progressBar *widget.ProgressBar, onComplete func(), btnSelectFile, btnSelectOutput, btnSlice *widget.Button, btnUpload *widget.Button, myWindow fyne.Window) { // 获取FFmpeg可执行文件的路径 ffmpegPath, cleanup, err := getFFmpegPath() if err != nil { log.Fatal(err) } startTime := time.Now() // 记录开始时间以便计算总耗时 log.Println("开始切割视频") // 构建FFmpeg命令行,配置输出为HLS格式,每5秒切一片 cmd := exec.Command( ffmpegPath, "-i", inputFile, // 输入文件 "-c", "copy", // 复制编解码器设置,避免重新编码 "-start_number", "0", // HLS段的起始编号 "-hls_time", "5", // 每个HLS段的最大时长(秒) "-hls_list_size", "0", // 播放列表中的最大段数,0表示无限制 "-f", "hls", // 输出格式为HLS "-hls_segment_filename", outputDir+"/%08d.ts", // 段文件的命名方式 outputDir+"/playlist.m3u8", // 输出的m3u8文件位置 ) // 设置管道以读取FFmpeg的stderr,用于解析进度信息 cmdReader, err := cmd.StderrPipe() if err != nil { log.Fatal("创建stderr管道失败:", err) } if err := cmd.Start(); err != nil { log.Fatalf("cmd.Start()失败: %s\n", err) } scanner := bufio.NewScanner(cmdReader) totalDuration := getTotalDuration(inputFile) // 获取视频的总时长 log.Printf("视频总时长%f\n", totalDuration) segment := math.Ceil(totalDuration / 5) // 计算所需的段数 log.Println(segment) // 在后台goroutine中监控FFmpeg的输出,更新进度条并处理完成状态 go func() { var completed bool for scanner.Scan() { line := scanner.Text() log.Println("FFmpeg输出:", line) // 解析FFmpeg的输出,更新进度条 if segmentCount := parseFFmpegProgressOutput(line); segmentCount > 0 { log.Println(segmentCount / segment) progressBar.SetValue(segmentCount / segment) } // 检查是否已完成切割 if checkFFmpegCompletion(line) { completed = true endTime := time.Now() // 记录完成时间 duration := endTime.Sub(startTime) // 计算总耗时 formattedDuration := formatDuration(duration) // 格式化耗时显示 progressBar.SetValue(1.0) // 设置进度条为完成状态 // 显示完成信息的对话框 dialog.ShowInformation("完成", fmt.Sprintf("✅ 视频切片完成!可以将切片直接上传云存储。\n总用时: %s\n存储位置:%s", formattedDuration, outputDir), myWindow) log.Println("视频切片完成") btnUpload.Enable() cleanup() // 确保在函数返回时删除临时目录 break } } if err := cmd.Wait(); err != nil { log.Fatalf("cmd.Wait()失败: %s\n", err) } if !completed { log.Println("❌ 视频切片可能未正确完成") } onComplete() // 调用完成回调 }() } // getTotalDuration 使用 FFmpeg 提取视频文件的总时长 // 参数: // // file string - 视频文件的路径 // // 返回值: // // float64 - 视频的总时长(单位:秒) func getTotalDuration(file string) float64 { // 获取 FFmpeg 可执行文件的路径 ffmpegPath, cleanup, err := getFFmpegPath() if err != nil { log.Fatal(err) } defer cleanup() // 确保在函数返回时删除临时目录 // 构建并启动 FFmpeg 命令,用于获取视频时长 cmd := exec.Command(ffmpegPath, "-i", file) // 由于 FFmpeg 在只有 "-i" 参数时将输出发送到 stderr,因此我们需要捕获 stderr stderr, err := cmd.StderrPipe() if err != nil { log.Fatal("Failed to get stderr pipe:", err) } // 启动 FFmpeg 命令 if err := cmd.Start(); err != nil { log.Println("Failed to start ffmpeg:", err) return 0.0 // 无法获取时长时返回 0.0 } // 使用 Scanner 从 stderr 中读取输出 scanner := bufio.NewScanner(stderr) // 正则表达式用于匹配 FFmpeg 输出的时长信息 durationRegex := regexp.MustCompile(`Duration: (\d{2}):(\d{2}):(\d{2}\.\d{2})`) var duration float64 = 0.0 for scanner.Scan() { // 寻找匹配的时长字符串,并解析为秒 matches := durationRegex.FindStringSubmatch(scanner.Text()) if matches != nil { hours, _ := strconv.Atoi(matches[1]) minutes, _ := strconv.Atoi(matches[2]) seconds, _ := strconv.ParseFloat(matches[3], 64) duration = float64(hours)*3600 + float64(minutes)*60 + seconds break // 找到时长后即刻跳出循环 } } // 等待 FFmpeg 命令执行完成,不处理命令结束状态 cmd.Wait() return duration } // parseFFmpegProgressOutput 函数解析 FFmpeg 进度输出行,返回匹配到的数字部分。 // 参数: // // line string - FFmpeg 进度输出的一行文本。 // // 返回值: // // float64 - 匹配到的数字转换为浮点数后返回,如果没有匹配到则返回 0。 func parseFFmpegProgressOutput(line string) float64 { re := regexp.MustCompile(`(\d+)\.ts`) // 使用正则表达式编译模式以匹配数字后跟.ts的字符串 matches := re.FindStringSubmatch(line) if len(matches) > 1 { number, err := strconv.Atoi(matches[1]) // 将提取的数字字符串转换为int类型 if err != nil { log.Println("Error converting string to integer:", err) // 记录转换错误日志 return 0 } return float64(number) // 返回匹配到的数字转换后的浮点数 } return 0 // 如果没有找到匹配的模式,返回0 } // checkFFmpegCompletion 函数用于检查 FFmpeg 处理进程是否完成。 // 它通过分析 FFmpeg 输出的行中是否包含特定的字符串来判断处理是否完成。 // // 参数: // line string - FFmpeg 输出的一行文本。 // // 返回值: // bool - 如果行中包含指定的字符串,则返回 true,表示处理已完成;否则返回 false。 func checkFFmpegCompletion(line string) bool { // 记录接收到的行,以便于调试 log.Println("line:", line) // 正则表达式匹配含有 "out#0/hls" 的行,这通常在 FFmpeg 输出最后阶段显示 re := regexp.MustCompile(`out#0/hls`) // 检查这一行是否匹配正则表达式 matchFound := re.MatchString(line) log.Println("re", matchFound) return matchFound } // formatDuration 将时间Duration格式化为易读的字符串 // 参数: // // d time.Duration - 需要格式化的时间长度 // // 返回值: // // string - 格式化后的时间字符串,格式为 "X小时X分X秒X毫秒",根据实际时间长度包含其中的项 func formatDuration(d time.Duration) string { // 将时间转换为总秒数 totalSeconds := int(d.Seconds()) // 计算小时、分钟和秒 hours := totalSeconds / 3600 minutes := (totalSeconds % 3600) / 60 seconds := totalSeconds % 60 // 获取毫秒数 milliseconds := int(d.Milliseconds()) % 1000 result := "" // 如果有小时,则添加到结果中 if hours > 0 { result += fmt.Sprintf("%d小时", hours) } // 如果有小时或者分钟不为0,则添加分钟到结果中 if minutes > 0 || hours > 0 { result += fmt.Sprintf("%d分", minutes) } // 如果秒不为0,或者同时小时和分钟都不为0,则添加秒到结果中 if seconds > 0 || (hours != 0 && minutes != 0) { result += fmt.Sprintf("%d秒", seconds) } // 总是添加毫秒到结果中 result += fmt.Sprintf("%d毫秒", milliseconds) return result } // 上传到Ucloud func upload(localPath string, keyPath string, progressBar *widget.ProgressBar, myWindow fyne.Window, onComplete func()) { fmt.Println("上传到Ucloud") progressBar.SetValue(0) fmt.Println("localPath:", localPath) fmt.Println("keyPath:", keyPath) // 统计 .ts .m3u8 文件数量 tsFiles, m3u8Files, err := countMediaFiles(localPath) if err != nil { log.Fatalf("Error counting media files: %s", err) } fmt.Printf("Total .ts files: %d\n", tsFiles) fmt.Printf("Total .m3u8 files: %d\n", m3u8Files) filesTotal := float64(tsFiles + m3u8Files) fmt.Printf("Total files: %d\n", int(filesTotal)) // 获取 us3cli 的路径 us3cliPath, cleanup, err := getUs3cliPath() if err != nil { log.Fatal(err) } defer cleanup() // 确保在函数返回时删除临时目录 bucket := "us3://jlntv-live/replay/" + keyPath // us3ConfigPath, cleanup, err := getUs3Config() // if err != nil { // log.Fatal(err) // } // defer cleanup() // 确保在函数返回时删除临时目录 // cmd := exec.Command(us3cliPath, "cp", localPath, bucket, "-r", "--parallel", "20", "--config", us3ConfigPath) cmd := exec.Command(us3cliPath, "cp", localPath, bucket, "-r", "--parallel", "20", "--accesskey", "TOKEN_6096c736-12b7-4c20-bfa5-e85e0e9c9b65", "--secretkey", "cc8a5965-3325-4231-9f64-a6626f624049", "--endpoint", "cn-bj.ufileos.com") //输出cmd fmt.Println(cmd.String()) stdout, err := cmd.StdoutPipe() if err != nil { log.Fatal("Error creating stdout pipe:", err) } if err := cmd.Start(); err != nil { log.Fatalf("Failed to start command: %s", err) } progress := 0.0 go func() { buffer := make([]byte, 4096) // 创建一个足够大的buffer for { n, err := stdout.Read(buffer) if n > 0 { fmt.Print("STDOUT:", string(buffer[:n])) // 打印实时输出 filesUploadedTotal, err := extractTotalValue(string(buffer[:n])) if err != nil { fmt.Println("Error:", err) } else { fmt.Println("Total:", filesUploadedTotal) } stats, err := parseUploadStats(string(buffer[:n])) if err != nil { fmt.Println("Error parsing upload stats:", err) } else { fmt.Printf("Uploaded: %d, Failed: %d, Total Size: %s, Speed: %s\n", stats.Successed, stats.Failed, stats.TotalSize, stats.AverageSpeed) if float64(stats.Successed) == filesTotal { progressBar.SetValue(1) showCustomDialog := func() { replayURL := fmt.Sprintf("https://video.jlntv.cn/replay/%s/playlist.m3u8", keyPath) copyBtn := widget.NewButton("复制地址", func() { fmt.Println("copyed:", replayURL) // dialog.ShowInformation("Submitted", "You submitted: "+input.Text, myWindow) clipboard := myWindow.Clipboard() clipboard.SetContent(replayURL) // showToast(myWindow, "地址已复制到剪贴板", 3*time.Second) dialog.ShowInformation("提示", "地址已复制到剪贴板!", myWindow) }) // 创建自定义内容的容器 content := container.NewVBox( widget.NewLabel("Upload Stats:"), widget.NewLabel(fmt.Sprintf("用时:%s秒", stats.Elapsed)), widget.NewLabel(fmt.Sprintf("成功:%d", stats.Successed)), widget.NewLabel(fmt.Sprintf("失败:%d", stats.Failed)), widget.NewLabel(fmt.Sprintf("平均速度:%s", stats.AverageSpeed)), widget.NewLabel(fmt.Sprintf("总大小:%s", stats.TotalSize)), widget.NewLabel(fmt.Sprintf("回看地址: %s", replayURL)), copyBtn, ) // 设置内容容器的最小尺寸 content.Resize(fyne.NewSize(500, 600)) // 你可以根据需要调整这个尺寸 // 创建并显示自定义对话框 customDialog := dialog.NewCustom("上传成功", "关闭", content, myWindow) customDialog.Show() } showCustomDialog() // dialog.ShowInformation("完成", fmt.Sprintf("✅ HLS上传完毕\n总用时:%s秒\n成功:%d 失败:%d 总大小:%s 平均速度:%s", stats.Elapsed, stats.Successed, stats.Failed, stats.TotalSize, stats.AverageSpeed), myWindow) fmt.Println("Upload completed!") } else { progress = filesUploadedTotal / filesTotal fmt.Println("Progress:", progress) progressBar.SetValue(progress) } } } if err != nil { break } } }() if err := cmd.Wait(); err != nil { log.Printf("Command finished with error: %v", err) } if onComplete != nil { onComplete() } } // extractTotalValue 函数从给定的文本中提取总值。 // // 参数: // // text string - 包含待搜索总值的文本。 // // 返回值: // // float64 - 从文本中提取到的总值,如果未找到则返回默认值200。 // error - 如果在提取过程中发生错误,则返回相应的错误;否则返回nil。 func extractTotalValue(text string) (float64, error) { // 使用正则表达式匹配文本中的总值部分 re := regexp.MustCompile(`Total:(\d+)`) matches := re.FindStringSubmatch(text) if len(matches) > 1 { // 将匹配到的总值字符串转换为整数 total, err := strconv.Atoi(matches[1]) if err != nil { // 如果转换过程中发生错误,返回错误信息 return 0, err } // 返回转换后的总值 return float64(total), nil } // 如果未在文本中找到总值,返回默认值200和一个错误信息 return 200, fmt.Errorf("no total found in the text") } // parseUploadStats 函数解析上传统计信息字符串,并返回一个包含上传统计详情的结构体指针。 // 参数 line 为待解析的上传统计信息字符串。 // 返回 *UploadStats 指向解析出的上传统计详情结构体。 // 返回 error 表示在解析过程中遇到的任何错误。 func parseUploadStats(line string) (*UploadStats, error) { stats := &UploadStats{} var err error // 初始化正则表达式用于匹配不同的上传统计信息。 successedRegex := regexp.MustCompile(`(\d+) Successed`) failedRegex := regexp.MustCompile(`(\d+) Failed`) sizeRegex := regexp.MustCompile(`Size: ([\d.]+ MB)`) speedRegex := regexp.MustCompile(`Average speed ([\d.]+ MB/s)`) elapsedRegex := regexp.MustCompile(`Elapsed\s*:\s*([\d.]+)s`) // 提取成功上传的文件数量。 successedMatches := successedRegex.FindStringSubmatch(line) if len(successedMatches) > 1 { stats.Successed, err = strconv.Atoi(successedMatches[1]) if err != nil { return nil, fmt.Errorf("error parsing successed number: %v", err) } } // 提取失败上传的文件数量。 failedMatches := failedRegex.FindStringSubmatch(line) if len(failedMatches) > 1 { stats.Failed, err = strconv.Atoi(failedMatches[1]) if err != nil { return nil, fmt.Errorf("error parsing failed number: %v", err) } } // 提取上传的总大小。 sizeMatches := sizeRegex.FindStringSubmatch(line) if len(sizeMatches) > 1 { stats.TotalSize = sizeMatches[1] } // 提取平均上传速度。 speedMatches := speedRegex.FindStringSubmatch(line) if len(speedMatches) > 1 { stats.AverageSpeed = speedMatches[1] } // 提取上传所花费的时间。 elapsedMatches := elapsedRegex.FindStringSubmatch(line) if len(elapsedMatches) > 1 { stats.Elapsed = elapsedMatches[1] } return stats, nil } // countMediaFiles 统计给定目录中 .ts 和 .m3u8 文件的数量。 // // 参数: // // directory string - 需要遍历统计文件的目录路径。 // // 返回值: // // int - .ts 文件的数量。 // int - .m3u8 文件的数量。 // error - 遍历目录过程中遇到的任何错误。 func countMediaFiles(directory string) (int, int, error) { var tsCount, m3u8Count int // 分别用于记录 .ts 和 .m3u8 文件的数量 // 使用 filepath.Walk 遍历指定目录及其子目录中的所有文件 err := filepath.Walk(directory, func(path string, info os.FileInfo, err error) error { // 检查遍历过程是否遇到错误 if err != nil { return err // 如果有错误,则终止遍历并返回该错误 } // 检查当前路径项是否为文件,不遍历目录 if !info.IsDir() { // 根据文件扩展名统计 .ts 和 .m3u8 文件数量 switch filepath.Ext(info.Name()) { case ".ts": tsCount++ // .ts 文件计数 case ".m3u8": m3u8Count++ // .m3u8 文件计数 } } return nil // 继续遍历下一个文件或目录 }) // 返回 .ts 和 .m3u8 文件的计数结果,以及遍历过程中可能遇到的错误 return tsCount, m3u8Count, err } // createURL 是一个用于创建并返回一个url.URL指针的函数。 // 它接受一个字符串类型的参数link,表示待解析的URL链接。 // 如果链接解析失败,将会记录错误信息并终止程序。 // // 参数: // // link string - 待解析的URL字符串。 // // 返回值: // // *url.URL - 解析后的URL结构体指针。 func createURL(link string) *url.URL { // 解析提供的链接 url, err := url.Parse(link) if err != nil { // 如果解析失败,记录错误信息并终止程序 log.Fatalf("Error parsing URL: %v", err) } return url }