拆分了文件
This commit is contained in:
parent
4abc315e3c
commit
89125db5a4
@ -5,4 +5,4 @@ Website = "https://iteay.top"
|
|||||||
Name = "HLS-builder"
|
Name = "HLS-builder"
|
||||||
ID = "top.iteay.hls-builder"
|
ID = "top.iteay.hls-builder"
|
||||||
Version = "1.0.0"
|
Version = "1.0.0"
|
||||||
Build = 32
|
Build = 33
|
||||||
|
26
fyne_metadata_init.go
Normal file
26
fyne_metadata_init.go
Normal file
File diff suppressed because one or more lines are too long
593
main.go
593
main.go
@ -1,7 +1,7 @@
|
|||||||
/*
|
/*
|
||||||
* @Author: BlackTeay
|
* @Author: BlackTeay
|
||||||
* @Date: 2024-04-30 09:37:39
|
* @Date: 2024-04-30 09:37:39
|
||||||
* @LastEditTime: 2024-05-08 14:43:47
|
* @LastEditTime: 2024-05-09 16:08:54
|
||||||
* @LastEditors: BlackTeay
|
* @LastEditors: BlackTeay
|
||||||
* @Description:
|
* @Description:
|
||||||
* @FilePath: /hls_builder/main.go
|
* @FilePath: /hls_builder/main.go
|
||||||
@ -10,23 +10,13 @@
|
|||||||
package main
|
package main
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"bufio"
|
|
||||||
"embed"
|
|
||||||
_ "embed"
|
_ "embed"
|
||||||
"errors"
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
|
|
||||||
"image/color"
|
|
||||||
"log"
|
"log"
|
||||||
"math"
|
|
||||||
"net/url"
|
"net/url"
|
||||||
"os"
|
"os"
|
||||||
"os/exec"
|
|
||||||
"path/filepath"
|
|
||||||
"regexp"
|
|
||||||
"runtime"
|
|
||||||
"strconv"
|
|
||||||
"time"
|
|
||||||
|
|
||||||
"fyne.io/fyne/v2"
|
"fyne.io/fyne/v2"
|
||||||
"fyne.io/fyne/v2/app"
|
"fyne.io/fyne/v2/app"
|
||||||
@ -34,135 +24,10 @@ import (
|
|||||||
"fyne.io/fyne/v2/dialog"
|
"fyne.io/fyne/v2/dialog"
|
||||||
"fyne.io/fyne/v2/layout"
|
"fyne.io/fyne/v2/layout"
|
||||||
"fyne.io/fyne/v2/storage"
|
"fyne.io/fyne/v2/storage"
|
||||||
"fyne.io/fyne/v2/theme"
|
|
||||||
"fyne.io/fyne/v2/widget"
|
"fyne.io/fyne/v2/widget"
|
||||||
"github.com/google/uuid"
|
"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 是应用程序的入口点。
|
// main 是应用程序的入口点。
|
||||||
func main() {
|
func main() {
|
||||||
// 打印当前工作目录
|
// 打印当前工作目录
|
||||||
@ -174,7 +39,7 @@ func main() {
|
|||||||
|
|
||||||
// 创建主窗口并设置大小
|
// 创建主窗口并设置大小
|
||||||
myWindow := myApp.NewWindow("HLS-builder 视频切片上传工具")
|
myWindow := myApp.NewWindow("HLS-builder 视频切片上传工具")
|
||||||
myWindow.Resize(fyne.NewSize(800, 400))
|
myWindow.Resize(fyne.NewSize(750, 550))
|
||||||
|
|
||||||
// 初始化UI组件:文件选择提示、输出路径选择提示、进度条
|
// 初始化UI组件:文件选择提示、输出路径选择提示、进度条
|
||||||
inputFile := widget.NewLabel("请选择mp4文件!")
|
inputFile := widget.NewLabel("请选择mp4文件!")
|
||||||
@ -288,460 +153,6 @@ func main() {
|
|||||||
myWindow.ShowAndRun()
|
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指针的函数。
|
// createURL 是一个用于创建并返回一个url.URL指针的函数。
|
||||||
// 它接受一个字符串类型的参数link,表示待解析的URL链接。
|
// 它接受一个字符串类型的参数link,表示待解析的URL链接。
|
||||||
// 如果链接解析失败,将会记录错误信息并终止程序。
|
// 如果链接解析失败,将会记录错误信息并终止程序。
|
||||||
|
101
paths.go
Normal file
101
paths.go
Normal file
@ -0,0 +1,101 @@
|
|||||||
|
/*
|
||||||
|
* @Author: BlackTeay
|
||||||
|
* @Date: 2024-05-09 15:41:11
|
||||||
|
* @LastEditTime: 2024-05-09 15:59:27
|
||||||
|
* @LastEditors: BlackTeay
|
||||||
|
* @Description:
|
||||||
|
* @FilePath: /hls_builder/paths.go
|
||||||
|
* Copyright 2024 JLNTV NMTD, All Rights Reserved.
|
||||||
|
*/
|
||||||
|
// paths.go
|
||||||
|
|
||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"embed"
|
||||||
|
"log"
|
||||||
|
"os"
|
||||||
|
"path/filepath"
|
||||||
|
"runtime"
|
||||||
|
)
|
||||||
|
|
||||||
|
//go:embed bin/*
|
||||||
|
var binFS embed.FS
|
||||||
|
|
||||||
|
// 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 := binFS.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 := binFS.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
|
||||||
|
}
|
246
slice.go
Normal file
246
slice.go
Normal file
@ -0,0 +1,246 @@
|
|||||||
|
/*
|
||||||
|
* @Author: BlackTeay
|
||||||
|
* @Date: 2024-05-09 15:47:30
|
||||||
|
* @LastEditTime: 2024-05-09 16:07:08
|
||||||
|
* @LastEditors: BlackTeay
|
||||||
|
* @Description:
|
||||||
|
* @FilePath: /hls_builder/slice.go
|
||||||
|
* Copyright 2024 JLNTV NMTD, All Rights Reserved.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bufio"
|
||||||
|
"fmt"
|
||||||
|
"log"
|
||||||
|
"math"
|
||||||
|
"os/exec"
|
||||||
|
"regexp"
|
||||||
|
"strconv"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"fyne.io/fyne/v2"
|
||||||
|
"fyne.io/fyne/v2/dialog"
|
||||||
|
"fyne.io/fyne/v2/widget"
|
||||||
|
)
|
||||||
|
|
||||||
|
// 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() // 调用完成回调
|
||||||
|
}()
|
||||||
|
}
|
||||||
|
|
||||||
|
// 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
|
||||||
|
}
|
||||||
|
|
||||||
|
// 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
|
||||||
|
}
|
||||||
|
|
||||||
|
// 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
|
||||||
|
}
|
54
theme.go
Normal file
54
theme.go
Normal file
@ -0,0 +1,54 @@
|
|||||||
|
/*
|
||||||
|
* @Author: BlackTeay
|
||||||
|
* @Date: 2024-05-09 15:40:58
|
||||||
|
* @LastEditTime: 2024-05-09 15:42:53
|
||||||
|
* @LastEditors: BlackTeay
|
||||||
|
* @Description:
|
||||||
|
* @FilePath: /hls_builder/theme.go
|
||||||
|
* Copyright 2024 JLNTV NMTD, All Rights Reserved.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
_ "embed"
|
||||||
|
"image/color"
|
||||||
|
|
||||||
|
"fyne.io/fyne/v2"
|
||||||
|
"fyne.io/fyne/v2/theme"
|
||||||
|
)
|
||||||
|
|
||||||
|
//go:embed "font/NotoSansSC-Regular.ttf"
|
||||||
|
var ttfBytes []byte
|
||||||
|
|
||||||
|
// 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)
|
||||||
|
}
|
267
upload.go
Normal file
267
upload.go
Normal file
@ -0,0 +1,267 @@
|
|||||||
|
/*
|
||||||
|
* @Author: BlackTeay
|
||||||
|
* @Date: 2024-05-09 15:47:52
|
||||||
|
* @LastEditTime: 2024-05-09 16:07:41
|
||||||
|
* @LastEditors: BlackTeay
|
||||||
|
* @Description:
|
||||||
|
* @FilePath: /hls_builder/upload.go
|
||||||
|
* Copyright 2024 JLNTV NMTD, All Rights Reserved.
|
||||||
|
*/
|
||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"log"
|
||||||
|
"os"
|
||||||
|
"os/exec"
|
||||||
|
"path/filepath"
|
||||||
|
"regexp"
|
||||||
|
"strconv"
|
||||||
|
|
||||||
|
"fyne.io/fyne/v2"
|
||||||
|
"fyne.io/fyne/v2/container"
|
||||||
|
"fyne.io/fyne/v2/dialog"
|
||||||
|
"fyne.io/fyne/v2/widget"
|
||||||
|
)
|
||||||
|
|
||||||
|
type UploadStats struct {
|
||||||
|
Successed int
|
||||||
|
Failed int
|
||||||
|
TotalSize string
|
||||||
|
AverageSpeed string
|
||||||
|
Elapsed string
|
||||||
|
}
|
||||||
|
|
||||||
|
// 上传到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()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 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
|
||||||
|
}
|
||||||
|
|
||||||
|
// 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")
|
||||||
|
}
|
||||||
|
|
||||||
|
// 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
|
||||||
|
}
|
Loading…
x
Reference in New Issue
Block a user