HLS-builder/main.go
2024-05-07 17:23:37 +08:00

729 lines
23 KiB
Go
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

/*
* @Author: BlackTeay
* @Date: 2024-04-30 09:37:39
* @LastEditTime: 2024-05-07 15:08:22
* @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"
"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/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
//go:embed config/userconfig.yaml
var configFS 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)
}
cleanup := func() {
os.RemoveAll(tmpDir) // 删除整个临时目录
}
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)
}
cleanup := func() {
os.RemoveAll(tmpDir) // 删除整个临时目录
}
return tmpFilePath, cleanup, nil
}
func getUs3Config() (string, func(), error) {
// 读取嵌入的配置文件
data, err := configFS.ReadFile("config/userconfig.yaml")
if err != nil {
log.Fatal(err)
}
// 创建临时目录并将us3cli文件内容写入其中
tmpDir, err := os.MkdirTemp("", "config")
if err != nil {
log.Fatal(err)
}
tmpFilePath := filepath.Join(tmpDir, "userconfig.yaml")
if err := os.WriteFile(tmpFilePath, data, 0755); err != nil {
log.Fatal(err)
}
cleanup := func() {
os.RemoveAll(tmpDir) // 删除整个临时目录
}
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, 600))
// 初始化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()
// 组合UI元素
content := container.NewVBox(
inputFile,
btnSelectFile,
outputDir,
btnSelectOutput,
sliceProgressBar,
btnSlice,
uploadLabel,
btnUpload,
uploadProgressBar,
)
// 设置窗口内容并显示
myWindow.SetContent(content)
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)
}
defer cleanup() // 确保在函数返回时删除临时目录
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()
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))
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
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()
}
}
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
}
return 200, fmt.Errorf("no total found in the text")
}
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`)
// Extracting number of files successed
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)
}
}
// Extracting number of files failed
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)
}
}
// Extracting total size
sizeMatches := sizeRegex.FindStringSubmatch(line)
if len(sizeMatches) > 1 {
stats.TotalSize = sizeMatches[1]
}
// Extracting average speed
speedMatches := speedRegex.FindStringSubmatch(line)
if len(speedMatches) > 1 {
stats.AverageSpeed = speedMatches[1]
}
// Extracting Elapsed
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
}