chore: 初始化仓库基线(AGENTS.md、git 规范、敏感文件排除)

This commit is contained in:
weijuesen
2026-08-10 22:30:53 +08:00
commit 84abf4454c
358 changed files with 75993 additions and 0 deletions
+172
View File
@@ -0,0 +1,172 @@
package service
import (
"bufio"
"context"
"fmt"
"io"
"log/slog"
"os/exec"
"silk-server-go/internal/model"
"gorm.io/gorm"
)
// TranscodeService ffmpeg 实时转码服务
type TranscodeService struct {
db *gorm.DB
s3 *S3Service
sem chan struct{} // 信号量,限制并发转码数为 4
}
// NewTranscodeService 创建转码服务,信号量默认容量 4
func NewTranscodeService(db *gorm.DB, s3 *S3Service) *TranscodeService {
return &TranscodeService{
db: db,
s3: s3,
sem: make(chan struct{}, 4),
}
}
// StreamLive 通过 ffmpeg 实时转码直播流(H264 re-encode for browser compatibility
func (s *TranscodeService) StreamLive(sourceURL string, writer io.Writer) error {
s.sem <- struct{}{}
defer func() { <-s.sem }()
ctx, cancel := context.WithCancel(context.Background())
defer cancel()
cmd := exec.CommandContext(ctx, "ffmpeg",
"-i", sourceURL,
"-c:v", "libx264",
"-preset", "ultrafast",
"-tune", "zerolatency",
"-b:v", "1M",
"-maxrate", "1.5M",
"-bufsize", "1M",
"-g", "30",
"-an",
"-f", "flv",
"pipe:1",
)
stdoutPipe, err := cmd.StdoutPipe()
if err != nil {
return fmt.Errorf("创建 stdout 管道失败: %w", err)
}
stderrPipe, err := cmd.StderrPipe()
if err != nil {
return fmt.Errorf("创建 stderr 管道失败: %w", err)
}
if err := cmd.Start(); err != nil {
return fmt.Errorf("启动 ffmpeg 失败: %w", err)
}
defer func() {
if cmd.ProcessState == nil || !cmd.ProcessState.Exited() {
_ = cmd.Process.Kill()
}
}()
go func() {
scanner := bufio.NewScanner(stderrPipe)
for scanner.Scan() {
slog.Info("ffmpeg-live", "msg", scanner.Text())
}
}()
if _, err := io.Copy(writer, stdoutPipe); err != nil {
cancel()
return fmt.Errorf("流式传输失败: %w", err)
}
if err := cmd.Wait(); err != nil {
if ctx.Err() != nil {
return ctx.Err()
}
return fmt.Errorf("ffmpeg 异常退出: %w", err)
}
return nil
}
// StreamClip 通过 ffmpeg 实时转码视频片段并流式写入 writer
// 流程:查库获取 S3 信息 → 生成 presigned URL → ffmpeg -c copy 转封装为 fragmented MP4 → 管道输出
func (s *TranscodeService) StreamClip(clipId string, writer io.Writer) error {
// 1. 查库获取 clip 的 s3Bucket + s3Key
var clip model.VideoClip
if err := s.db.Where("id = ?", clipId).First(&clip).Error; err != nil {
return fmt.Errorf("视频片段不存在: %w", err)
}
if clip.S3Bucket == nil || clip.S3Key == nil || *clip.S3Bucket == "" || *clip.S3Key == "" {
return fmt.Errorf("该片段没有 S3 对象")
}
// 2. 生成 S3 presigned URL
presignedURL, err := s.s3.GetPresignedURL(*clip.S3Bucket, *clip.S3Key)
if err != nil {
return fmt.Errorf("生成 presigned URL 失败: %w", err)
}
// 3. 获取信号量(并发限制 4
s.sem <- struct{}{}
defer func() { <-s.sem }()
// 4. 启动 ffmpeg: ffmpeg -i <url> -c copy -movflags frag_keyframe+empty_moov -f mp4 pipe:1
ctx, cancel := context.WithCancel(context.Background())
defer cancel()
cmd := exec.CommandContext(ctx, "ffmpeg",
"-i", presignedURL,
"-c", "copy",
"-movflags", "frag_keyframe+empty_moov",
"-f", "mp4",
"pipe:1",
)
stdoutPipe, err := cmd.StdoutPipe()
if err != nil {
return fmt.Errorf("创建 stdout 管道失败: %w", err)
}
stderrPipe, err := cmd.StderrPipe()
if err != nil {
return fmt.Errorf("创建 stderr 管道失败: %w", err)
}
if err := cmd.Start(); err != nil {
return fmt.Errorf("启动 ffmpeg 失败: %w", err)
}
// 确保 ffmpeg 进程被清理(如果还在运行则 kill)
defer func() {
if cmd.ProcessState == nil || !cmd.ProcessState.Exited() {
_ = cmd.Process.Kill()
}
}()
// 5. 记录 ffmpeg stderr 日志
go func() {
scanner := bufio.NewScanner(stderrPipe)
for scanner.Scan() {
slog.Info("ffmpeg", "clipId", clipId, "msg", scanner.Text())
}
}()
// 6. 流式传输:io.Copy(writer, ffmpeg.Stdout)
if _, err := io.Copy(writer, stdoutPipe); err != nil {
cancel() // 取消 context,终止 ffmpeg
return fmt.Errorf("流式传输失败: %w", err)
}
// 7. 等待 ffmpeg 结束,检查退出码
if err := cmd.Wait(); err != nil {
if ctx.Err() != nil {
return ctx.Err()
}
return fmt.Errorf("ffmpeg 异常退出: %w", err)
}
return nil
}