/* * @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 }