/** * package mediaserver * * FFmpeg 实时转码器 * 将 WebM (VP8/Opus) 转码为 FLV (H.264/AAC) * 用于处理不支持 H.264 编码的浏览器发送的视频流 */ package mediaserver import ( "bytes" "fmt" "io" "log" "os/exec" "sync" "time" ) // TranscoderConfig 转码器配置 type TranscoderConfig struct { // 视频配置 VideoCodec string // 输出视频编解码器,默认 libx264 VideoPreset string // x264 预设,默认 ultrafast VideoBitrate string // 视频码率,默认 500k // 音频配置 AudioCodec string // 输出音频编解码器,默认 aac AudioBitrate string // 音频码率,默认 64k AudioSampleRate int // 音频采样率,默认 44100 AudioChannels int // 音频通道数,默认 2 } // DefaultTranscoderConfig 默认转码配置 func DefaultTranscoderConfig() *TranscoderConfig { return &TranscoderConfig{ VideoCodec: "libx264", VideoPreset: "ultrafast", VideoBitrate: "500k", AudioCodec: "aac", AudioBitrate: "64k", AudioSampleRate: 44100, AudioChannels: 2, } } // FFmpegTranscoder FFmpeg 转码器 type FFmpegTranscoder struct { config *TranscoderConfig ffmpegPath string cmd *exec.Cmd stdin io.WriteCloser stdout io.ReadCloser stderr io.ReadCloser outputChan chan []byte stopChan chan struct{} mu sync.Mutex running bool // 统计信息 inputBytes int64 outputBytes int64 startTime time.Time } // NewFFmpegTranscoder 创建新的转码器 func NewFFmpegTranscoder(config *TranscoderConfig) (*FFmpegTranscoder, error) { if config == nil { config = DefaultTranscoderConfig() } // 确保 FFmpeg 可用 ffmpegPath, err := EnsureFFmpeg() if err != nil { return nil, fmt.Errorf("FFmpeg 不可用: %w", err) } return &FFmpegTranscoder{ config: config, ffmpegPath: ffmpegPath, outputChan: make(chan []byte, 100), stopChan: make(chan struct{}), }, nil } // Start 启动转码器 func (t *FFmpegTranscoder) Start() error { t.mu.Lock() defer t.mu.Unlock() if t.running { return fmt.Errorf("转码器已在运行") } // 构建 FFmpeg 命令 // ffmpeg -f webm -i pipe:0 -c:v libx264 -preset ultrafast -tune zerolatency -c:a aac -f flv pipe:1 args := []string{ "-f", "webm", // 输入格式 "-i", "pipe:0", // 从 stdin 读取 "-c:v", t.config.VideoCodec, "-preset", t.config.VideoPreset, "-tune", "zerolatency", // 零延迟模式 "-b:v", t.config.VideoBitrate, "-c:a", t.config.AudioCodec, "-b:a", t.config.AudioBitrate, "-ar", fmt.Sprintf("%d", t.config.AudioSampleRate), "-ac", fmt.Sprintf("%d", t.config.AudioChannels), "-f", "flv", // 输出格式 "pipe:1", // 输出到 stdout } t.cmd = exec.Command(t.ffmpegPath, args...) var err error // 获取 stdin t.stdin, err = t.cmd.StdinPipe() if err != nil { return fmt.Errorf("获取 stdin 失败: %w", err) } // 获取 stdout t.stdout, err = t.cmd.StdoutPipe() if err != nil { return fmt.Errorf("获取 stdout 失败: %w", err) } // 获取 stderr t.stderr, err = t.cmd.StderrPipe() if err != nil { return fmt.Errorf("获取 stderr 失败: %w", err) } // 启动 FFmpeg 进程 if err := t.cmd.Start(); err != nil { return fmt.Errorf("启动 FFmpeg 失败: %w", err) } t.running = true t.startTime = time.Now() log.Printf("🎬 [FFmpegTranscoder] 转码器已启动, PID=%d", t.cmd.Process.Pid) // 启动输出读取协程 go t.readOutput() // 启动 stderr 读取协程(用于日志) go t.readStderr() // 启动进程监控协程 go t.monitor() return nil } // Stop 停止转码器 func (t *FFmpegTranscoder) Stop() error { t.mu.Lock() defer t.mu.Unlock() if !t.running { return nil } t.running = false close(t.stopChan) // 关闭 stdin,通知 FFmpeg 输入结束 if t.stdin != nil { t.stdin.Close() } // 等待进程结束 if t.cmd != nil && t.cmd.Process != nil { t.cmd.Process.Kill() t.cmd.Wait() } // 关闭输出通道 close(t.outputChan) duration := time.Since(t.startTime) log.Printf("🛑 [FFmpegTranscoder] 转码器已停止, 运行时间=%v, 输入=%d bytes, 输出=%d bytes", duration, t.inputBytes, t.outputBytes) return nil } // Write 写入 WebM 数据 func (t *FFmpegTranscoder) Write(data []byte) (int, error) { t.mu.Lock() if !t.running { t.mu.Unlock() return 0, fmt.Errorf("转码器未运行") } stdin := t.stdin t.mu.Unlock() n, err := stdin.Write(data) if err != nil { return n, err } t.inputBytes += int64(n) return n, nil } // Output 获取输出通道 func (t *FFmpegTranscoder) Output() <-chan []byte { return t.outputChan } // readOutput 读取 FFmpeg 输出 func (t *FFmpegTranscoder) readOutput() { buf := make([]byte, 64*1024) // 64KB 缓冲区 for { select { case <-t.stopChan: return default: } n, err := t.stdout.Read(buf) if err != nil { if err != io.EOF { log.Printf("⚠️ [FFmpegTranscoder] 读取输出错误: %v", err) } return } if n > 0 { t.outputBytes += int64(n) // 复制数据并发送到通道 data := make([]byte, n) copy(data, buf[:n]) select { case t.outputChan <- data: default: // 通道满,丢弃数据 log.Printf("⚠️ [FFmpegTranscoder] 输出通道满,丢弃 %d bytes", n) } } } } // readStderr 读取 FFmpeg 错误输出 func (t *FFmpegTranscoder) readStderr() { buf := new(bytes.Buffer) io.Copy(buf, t.stderr) if buf.Len() > 0 { // 只在有错误时输出 output := buf.String() if len(output) > 500 { output = output[:500] + "..." } log.Printf("📋 [FFmpegTranscoder] FFmpeg 输出: %s", output) } } // monitor 监控 FFmpeg 进程 func (t *FFmpegTranscoder) monitor() { err := t.cmd.Wait() t.mu.Lock() wasRunning := t.running t.running = false t.mu.Unlock() if wasRunning { if err != nil { log.Printf("⚠️ [FFmpegTranscoder] FFmpeg 进程异常退出: %v", err) } else { log.Printf("ℹ️ [FFmpegTranscoder] FFmpeg 进程正常退出") } } } // IsRunning 检查转码器是否运行中 func (t *FFmpegTranscoder) IsRunning() bool { t.mu.Lock() defer t.mu.Unlock() return t.running } // Stats 获取统计信息 func (t *FFmpegTranscoder) Stats() (inputBytes, outputBytes int64, duration time.Duration) { t.mu.Lock() defer t.mu.Unlock() return t.inputBytes, t.outputBytes, time.Since(t.startTime) } // TranscoderPool 转码器池,管理多个转码器实例 type TranscoderPool struct { transcoders map[string]*FFmpegTranscoder mu sync.RWMutex config *TranscoderConfig } // NewTranscoderPool 创建转码器池 func NewTranscoderPool(config *TranscoderConfig) *TranscoderPool { if config == nil { config = DefaultTranscoderConfig() } return &TranscoderPool{ transcoders: make(map[string]*FFmpegTranscoder), config: config, } } // Get 获取或创建转码器 func (p *TranscoderPool) Get(streamID string) (*FFmpegTranscoder, error) { p.mu.Lock() defer p.mu.Unlock() if t, exists := p.transcoders[streamID]; exists && t.IsRunning() { return t, nil } // 创建新的转码器 t, err := NewFFmpegTranscoder(p.config) if err != nil { return nil, err } if err := t.Start(); err != nil { return nil, err } p.transcoders[streamID] = t return t, nil } // Release 释放转码器 func (p *TranscoderPool) Release(streamID string) { p.mu.Lock() defer p.mu.Unlock() if t, exists := p.transcoders[streamID]; exists { t.Stop() delete(p.transcoders, streamID) } } // ReleaseAll 释放所有转码器 func (p *TranscoderPool) ReleaseAll() { p.mu.Lock() defer p.mu.Unlock() for id, t := range p.transcoders { t.Stop() delete(p.transcoders, id) } } // Count 获取活跃转码器数量 func (p *TranscoderPool) Count() int { p.mu.RLock() defer p.mu.RUnlock() return len(p.transcoders) }