HLS-builder/slice.go
2024-05-09 16:11:13 +08:00

247 lines
8.2 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-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
}