Files
nl-im-service/internal/mediaserver/ffmpeg_transcoder.go

370 lines
7.7 KiB
Go
Raw Blame History

This file contains invisible Unicode characters
This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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.
/**
* 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)
}