Replace internal decoder with ffplay subprocess
This commit is contained in:
parent
3d43123840
commit
4929918d4b
422
main.go
422
main.go
|
|
@ -3704,24 +3704,16 @@ func buildVideoPane(state *appState, min fyne.Size, src *videoSource, onCover fu
|
||||||
}
|
}
|
||||||
|
|
||||||
type playSession struct {
|
type playSession struct {
|
||||||
path string
|
path string
|
||||||
fps float64
|
fps float64
|
||||||
width int
|
volume float64
|
||||||
height int
|
muted bool
|
||||||
targetW int
|
paused bool
|
||||||
targetH int
|
current float64
|
||||||
volume float64
|
mu sync.Mutex
|
||||||
muted bool
|
cmd *exec.Cmd
|
||||||
paused bool
|
stdin io.WriteCloser
|
||||||
current float64
|
done chan struct{}
|
||||||
stop chan struct{}
|
|
||||||
done chan struct{}
|
|
||||||
prog func(float64)
|
|
||||||
img *canvas.Image
|
|
||||||
mu sync.Mutex
|
|
||||||
videoCmd *exec.Cmd
|
|
||||||
audioCmd *exec.Cmd
|
|
||||||
frameN int
|
|
||||||
}
|
}
|
||||||
|
|
||||||
var audioCtxGlobal struct {
|
var audioCtxGlobal struct {
|
||||||
|
|
@ -3743,42 +3735,26 @@ func newPlaySession(path string, w, h int, fps float64, targetW, targetH int, pr
|
||||||
fmt.Printf("═══════════════════════════════════════════════════════\n")
|
fmt.Printf("═══════════════════════════════════════════════════════\n")
|
||||||
fmt.Printf("📁 Video: %s\n", filepath.Base(path))
|
fmt.Printf("📁 Video: %s\n", filepath.Base(path))
|
||||||
fmt.Printf("📐 Source: %dx%d @ %.2f fps\n", w, h, fps)
|
fmt.Printf("📐 Source: %dx%d @ %.2f fps\n", w, h, fps)
|
||||||
fmt.Printf("🎯 Target: %dx%d\n", targetW, targetH)
|
fmt.Printf("🎯 Playback via ffplay subprocess\n")
|
||||||
fmt.Printf("═══════════════════════════════════════════════════════\n\n")
|
fmt.Printf("═══════════════════════════════════════════════════════\n\n")
|
||||||
|
|
||||||
// Validate input parameters
|
|
||||||
if fps <= 0 {
|
|
||||||
fps = 24
|
|
||||||
fmt.Printf("⚠️ Invalid FPS (%.2f), defaulting to 24\n", fps)
|
|
||||||
}
|
|
||||||
if targetW <= 0 {
|
|
||||||
targetW = 640
|
|
||||||
fmt.Printf("⚠️ Invalid target width (%d), defaulting to 640\n", targetW)
|
|
||||||
}
|
|
||||||
if targetH <= 0 {
|
|
||||||
targetH = int(float64(targetW) * (float64(h) / float64(utils.MaxInt(w, 1))))
|
|
||||||
fmt.Printf("⚠️ Invalid target height (%d), calculating to %d\n", targetH, targetH)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Check if video file exists
|
// Check if video file exists
|
||||||
if _, err := os.Stat(path); os.IsNotExist(err) {
|
if _, err := os.Stat(path); os.IsNotExist(err) {
|
||||||
fmt.Printf("❌ ERROR: Video file does not exist: %s\n", path)
|
fmt.Printf("❌ ERROR: Video file does not exist: %s\n", path)
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if _, err := exec.LookPath("ffplay"); err != nil {
|
||||||
|
fmt.Printf("❌ ERROR: ffplay not found in PATH: %v\n", err)
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
fmt.Printf("✅ Play session created successfully\n")
|
fmt.Printf("✅ Play session created successfully\n")
|
||||||
return &playSession{
|
return &playSession{
|
||||||
path: path,
|
path: path,
|
||||||
fps: fps,
|
fps: fps,
|
||||||
width: w,
|
volume: 100,
|
||||||
height: h,
|
done: make(chan struct{}),
|
||||||
targetW: targetW,
|
|
||||||
targetH: targetH,
|
|
||||||
volume: 100,
|
|
||||||
stop: make(chan struct{}),
|
|
||||||
done: make(chan struct{}),
|
|
||||||
prog: prog,
|
|
||||||
img: img,
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -3786,19 +3762,27 @@ func (p *playSession) Play() {
|
||||||
fmt.Printf("▶️ PLAY called (current position: %.2fs)\n", p.current)
|
fmt.Printf("▶️ PLAY called (current position: %.2fs)\n", p.current)
|
||||||
p.mu.Lock()
|
p.mu.Lock()
|
||||||
defer p.mu.Unlock()
|
defer p.mu.Unlock()
|
||||||
if p.videoCmd == nil && p.audioCmd == nil {
|
if p.cmd == nil {
|
||||||
fmt.Printf("⚡ Starting playback from scratch...\n")
|
fmt.Printf("⚡ Starting ffplay subprocess...\n")
|
||||||
p.startLocked(p.current)
|
p.startFFplayLocked(p.current)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
fmt.Printf("▶️ Resuming playback\n")
|
// Toggle pause/resume by sending "p" to stdin
|
||||||
p.paused = false
|
if p.stdin != nil {
|
||||||
|
_, _ = p.stdin.Write([]byte("p"))
|
||||||
|
p.paused = !p.paused
|
||||||
|
fmt.Printf("⏯️ Toggled pause -> %v\n", p.paused)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (p *playSession) Pause() {
|
func (p *playSession) Pause() {
|
||||||
p.mu.Lock()
|
p.mu.Lock()
|
||||||
defer p.mu.Unlock()
|
defer p.mu.Unlock()
|
||||||
p.paused = true
|
if p.stdin != nil {
|
||||||
|
_, _ = p.stdin.Write([]byte("p"))
|
||||||
|
p.paused = !p.paused
|
||||||
|
fmt.Printf("⏸️ Pause toggle -> %v\n", p.paused)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (p *playSession) Seek(offset float64) {
|
func (p *playSession) Seek(offset float64) {
|
||||||
|
|
@ -3807,22 +3791,9 @@ func (p *playSession) Seek(offset float64) {
|
||||||
if offset < 0 {
|
if offset < 0 {
|
||||||
offset = 0
|
offset = 0
|
||||||
}
|
}
|
||||||
paused := p.paused
|
|
||||||
p.current = offset
|
p.current = offset
|
||||||
p.stopLocked()
|
p.stopLocked()
|
||||||
p.startLocked(p.current)
|
p.startFFplayLocked(p.current)
|
||||||
p.paused = paused
|
|
||||||
if p.paused {
|
|
||||||
// Ensure loops honor paused right after restart.
|
|
||||||
time.AfterFunc(30*time.Millisecond, func() {
|
|
||||||
p.mu.Lock()
|
|
||||||
defer p.mu.Unlock()
|
|
||||||
p.paused = true
|
|
||||||
})
|
|
||||||
}
|
|
||||||
if p.prog != nil {
|
|
||||||
p.prog(p.current)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (p *playSession) SetVolume(v float64) {
|
func (p *playSession) SetVolume(v float64) {
|
||||||
|
|
@ -3840,41 +3811,15 @@ func (p *playSession) SetVolume(v float64) {
|
||||||
} else {
|
} else {
|
||||||
p.muted = true
|
p.muted = true
|
||||||
}
|
}
|
||||||
|
// Runtime volume changes are not pushed to ffplay; a restart or pause toggle will pick it up.
|
||||||
}
|
}
|
||||||
|
|
||||||
// StepFrame steps forward or backward by one frame
|
// StepFrame steps forward or backward by one frame
|
||||||
// direction: 1 for forward, -1 for backward
|
// direction: 1 for forward, -1 for backward
|
||||||
func (p *playSession) StepFrame(direction int) {
|
func (p *playSession) StepFrame(direction int) {
|
||||||
p.mu.Lock()
|
// Approximate by seeking one frame forward/backward
|
||||||
defer p.mu.Unlock()
|
step := 1.0 / math.Max(p.fps, 24)
|
||||||
|
p.Seek(p.current + float64(direction)*step)
|
||||||
// Ensure we're paused for frame stepping
|
|
||||||
if !p.paused {
|
|
||||||
p.paused = true
|
|
||||||
}
|
|
||||||
|
|
||||||
// Calculate new position (1 frame = 1/fps seconds)
|
|
||||||
frameDuration := 1.0 / p.fps
|
|
||||||
newPos := p.current + (float64(direction) * frameDuration)
|
|
||||||
if newPos < 0 {
|
|
||||||
newPos = 0
|
|
||||||
}
|
|
||||||
|
|
||||||
p.current = newPos
|
|
||||||
p.stopLocked()
|
|
||||||
p.startLocked(p.current)
|
|
||||||
p.paused = true
|
|
||||||
|
|
||||||
// Ensure paused state sticks
|
|
||||||
time.AfterFunc(30*time.Millisecond, func() {
|
|
||||||
p.mu.Lock()
|
|
||||||
defer p.mu.Unlock()
|
|
||||||
p.paused = true
|
|
||||||
})
|
|
||||||
|
|
||||||
if p.prog != nil {
|
|
||||||
p.prog(p.current)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// GetCurrentPosition returns the current playback position
|
// GetCurrentPosition returns the current playback position
|
||||||
|
|
@ -3891,288 +3836,49 @@ func (p *playSession) Stop() {
|
||||||
}
|
}
|
||||||
|
|
||||||
func (p *playSession) stopLocked() {
|
func (p *playSession) stopLocked() {
|
||||||
select {
|
if p.cmd != nil && p.cmd.Process != nil {
|
||||||
case <-p.stop:
|
_ = p.cmd.Process.Kill()
|
||||||
default:
|
_, _ = p.cmd.Process.Wait()
|
||||||
close(p.stop)
|
|
||||||
}
|
}
|
||||||
if p.videoCmd != nil && p.videoCmd.Process != nil {
|
p.cmd = nil
|
||||||
_ = p.videoCmd.Process.Kill()
|
p.stdin = nil
|
||||||
_ = p.videoCmd.Wait()
|
|
||||||
}
|
|
||||||
if p.audioCmd != nil && p.audioCmd.Process != nil {
|
|
||||||
_ = p.audioCmd.Process.Kill()
|
|
||||||
_ = p.audioCmd.Wait()
|
|
||||||
}
|
|
||||||
p.videoCmd = nil
|
|
||||||
p.audioCmd = nil
|
|
||||||
p.stop = make(chan struct{})
|
|
||||||
p.done = make(chan struct{})
|
p.done = make(chan struct{})
|
||||||
}
|
}
|
||||||
|
|
||||||
func (p *playSession) startLocked(offset float64) {
|
func (p *playSession) startFFplayLocked(offset float64) {
|
||||||
p.paused = false
|
|
||||||
p.current = offset
|
|
||||||
p.frameN = 0
|
|
||||||
logging.Debug(logging.CatFFMPEG, "playSession start path=%s offset=%.3f fps=%.3f target=%dx%d", p.path, offset, p.fps, p.targetW, p.targetH)
|
|
||||||
p.runVideo(offset)
|
|
||||||
p.runAudio(offset)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (p *playSession) runVideo(offset float64) {
|
|
||||||
fmt.Printf("📹 Starting video decode pipeline...\n")
|
|
||||||
fmt.Printf(" - Resolution: %dx%d\n", p.targetW, p.targetH)
|
|
||||||
fmt.Printf(" - Frame rate: %.2f fps\n", p.fps)
|
|
||||||
fmt.Printf(" - Offset: %.2fs\n", offset)
|
|
||||||
|
|
||||||
var stderr bytes.Buffer
|
var stderr bytes.Buffer
|
||||||
args := []string{
|
args := []string{
|
||||||
"-hide_banner", "-loglevel", "error",
|
"-hide_banner",
|
||||||
|
"-loglevel", "warning",
|
||||||
|
"-autoexit",
|
||||||
"-ss", fmt.Sprintf("%.3f", offset),
|
"-ss", fmt.Sprintf("%.3f", offset),
|
||||||
"-i", p.path,
|
"-i", p.path,
|
||||||
"-vf", fmt.Sprintf("scale=%d:%d:flags=bilinear", p.targetW, p.targetH),
|
"-window_title", "VT Player",
|
||||||
"-f", "rawvideo",
|
|
||||||
"-pix_fmt", "rgb24",
|
|
||||||
"-r", fmt.Sprintf("%.3f", p.fps),
|
|
||||||
"-vsync", "0", // Avoid frame duplication
|
|
||||||
"-",
|
|
||||||
}
|
}
|
||||||
fmt.Printf("🔧 FFmpeg command: ffmpeg %s\n", strings.Join(args, " "))
|
if p.volume > 0 {
|
||||||
|
args = append([]string{"-volume", fmt.Sprintf("%d", int(p.volume))}, args...)
|
||||||
cmd := exec.Command("ffmpeg", args...)
|
}
|
||||||
|
cmd := exec.Command("ffplay", args...)
|
||||||
cmd.Stderr = &stderr
|
cmd.Stderr = &stderr
|
||||||
stdout, err := cmd.StdoutPipe()
|
stdin, err := cmd.StdinPipe()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
fmt.Printf("❌ ERROR: Failed to create video pipe: %v\n", err)
|
fmt.Printf("❌ ERROR: failed to create ffplay stdin: %v\n", err)
|
||||||
logging.Debug(logging.CatFFMPEG, "video pipe error: %v", err)
|
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
cmd.Stdout = os.Stdout
|
||||||
fmt.Printf("⚡ Starting FFmpeg process...\n")
|
|
||||||
startTime := time.Now()
|
|
||||||
if err := cmd.Start(); err != nil {
|
if err := cmd.Start(); err != nil {
|
||||||
errMsg := strings.TrimSpace(stderr.String())
|
fmt.Printf("❌ ERROR: ffplay failed to start: %v\n", err)
|
||||||
fmt.Printf("❌ ERROR: FFmpeg failed to start: %v\n", err)
|
if msg := strings.TrimSpace(stderr.String()); msg != "" {
|
||||||
if errMsg != "" {
|
fmt.Printf(" ffplay stderr: %s\n", msg)
|
||||||
fmt.Printf(" FFmpeg error: %s\n", errMsg)
|
|
||||||
}
|
}
|
||||||
// Check if ffmpeg is available
|
|
||||||
if _, pathErr := exec.LookPath("ffmpeg"); pathErr != nil {
|
|
||||||
fmt.Printf("❌ FATAL: ffmpeg not found in PATH: %v\n", pathErr)
|
|
||||||
}
|
|
||||||
logging.Debug(logging.CatFFMPEG, "video start failed: %v (%s)", err, errMsg)
|
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
fmt.Printf("✅ FFmpeg started (PID: %d) in %.3fs\n", cmd.Process.Pid, time.Since(startTime).Seconds())
|
p.cmd = cmd
|
||||||
// Pace frames to the source frame rate instead of hammering refreshes as fast as possible.
|
p.stdin = stdin
|
||||||
frameDur := time.Second
|
p.paused = false
|
||||||
if p.fps > 0 {
|
|
||||||
frameDur = time.Duration(float64(time.Second) / math.Max(p.fps, 0.1))
|
|
||||||
}
|
|
||||||
nextFrameAt := time.Now()
|
|
||||||
p.videoCmd = cmd
|
|
||||||
frameSize := p.targetW * p.targetH * 3
|
|
||||||
buf := make([]byte, frameSize)
|
|
||||||
fmt.Printf("📦 Frame buffer allocated: %d bytes (%.2f MB)\n", frameSize, float64(frameSize)/(1024*1024))
|
|
||||||
|
|
||||||
go func() {
|
go func() {
|
||||||
defer cmd.Process.Kill()
|
_ = cmd.Wait()
|
||||||
|
close(p.done)
|
||||||
// Performance monitoring variables
|
|
||||||
var lastFPSReport time.Time
|
|
||||||
var fpsCounter int
|
|
||||||
loopStart := time.Now()
|
|
||||||
|
|
||||||
fmt.Printf("🔄 Frame decode loop started\n")
|
|
||||||
|
|
||||||
for {
|
|
||||||
select {
|
|
||||||
case <-p.stop:
|
|
||||||
fmt.Printf("⏹️ Video decode loop stopped\n")
|
|
||||||
logging.Debug(logging.CatFFMPEG, "video loop stop")
|
|
||||||
return
|
|
||||||
default:
|
|
||||||
}
|
|
||||||
if p.paused {
|
|
||||||
time.Sleep(30 * time.Millisecond)
|
|
||||||
nextFrameAt = time.Now().Add(frameDur)
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
readStart := time.Now()
|
|
||||||
_, err := io.ReadFull(stdout, buf)
|
|
||||||
readDuration := time.Since(readStart)
|
|
||||||
|
|
||||||
if err != nil {
|
|
||||||
if errors.Is(err, io.EOF) {
|
|
||||||
fmt.Printf("📺 Video playback completed (reached end)\n")
|
|
||||||
return
|
|
||||||
}
|
|
||||||
msg := strings.TrimSpace(stderr.String())
|
|
||||||
fmt.Printf("❌ ERROR: Frame read failed: %v\n", err)
|
|
||||||
if msg != "" {
|
|
||||||
fmt.Printf(" FFmpeg error: %s\n", msg)
|
|
||||||
}
|
|
||||||
logging.Debug(logging.CatFFMPEG, "video read failed: %v (%s)", err, msg)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
// Track first frame timing
|
|
||||||
if p.frameN == 0 {
|
|
||||||
elapsed := time.Since(loopStart)
|
|
||||||
fmt.Printf("🎞️ FIRST FRAME decoded in %.3fs (read: %.3fs)\n", elapsed.Seconds(), readDuration.Seconds())
|
|
||||||
}
|
|
||||||
|
|
||||||
// Improved frame pacing - use a more stable timing approach
|
|
||||||
now := time.Now()
|
|
||||||
if now.Before(nextFrameAt) {
|
|
||||||
time.Sleep(nextFrameAt.Sub(now))
|
|
||||||
}
|
|
||||||
nextFrameAt = nextFrameAt.Add(frameDur)
|
|
||||||
|
|
||||||
// Allocate a fresh frame to avoid concurrent texture reuse issues.
|
|
||||||
frame := image.NewRGBA(image.Rect(0, 0, p.targetW, p.targetH))
|
|
||||||
utils.CopyRGBToRGBA(frame.Pix, buf)
|
|
||||||
|
|
||||||
renderStart := time.Now()
|
|
||||||
fyne.CurrentApp().Driver().DoFromGoroutine(func() {
|
|
||||||
if p.img != nil {
|
|
||||||
// Ensure we render the live frame, not a stale resource preview.
|
|
||||||
p.img.Resource = nil
|
|
||||||
p.img.File = ""
|
|
||||||
p.img.Image = frame
|
|
||||||
p.img.Refresh()
|
|
||||||
}
|
|
||||||
}, false)
|
|
||||||
renderDuration := time.Since(renderStart)
|
|
||||||
|
|
||||||
// Log first few frames in detail
|
|
||||||
if p.frameN < 3 {
|
|
||||||
fmt.Printf("🖼️ Frame %d: decode=%.3fs render=%.3fs\n", p.frameN+1, readDuration.Seconds(), renderDuration.Seconds())
|
|
||||||
logging.Debug(logging.CatFFMPEG, "video frame %d drawn (%.2fs)", p.frameN+1, p.current)
|
|
||||||
}
|
|
||||||
|
|
||||||
p.frameN++
|
|
||||||
fpsCounter++
|
|
||||||
|
|
||||||
// FPS report every 2 seconds
|
|
||||||
if time.Since(lastFPSReport) >= 2*time.Second {
|
|
||||||
fps := float64(fpsCounter) / time.Since(lastFPSReport).Seconds()
|
|
||||||
fmt.Printf("📊 Performance: %.1f FPS (target: %.1f FPS) | Frames: %d\n", fps, p.fps, p.frameN)
|
|
||||||
fpsCounter = 0
|
|
||||||
lastFPSReport = time.Now()
|
|
||||||
}
|
|
||||||
|
|
||||||
if p.fps > 0 {
|
|
||||||
p.current = offset + (float64(p.frameN) / p.fps)
|
|
||||||
}
|
|
||||||
if p.prog != nil {
|
|
||||||
p.prog(p.current)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}()
|
|
||||||
}
|
|
||||||
|
|
||||||
func (p *playSession) runAudio(offset float64) {
|
|
||||||
const sampleRate = 48000
|
|
||||||
const channels = 2
|
|
||||||
const bytesPerSample = 2
|
|
||||||
var stderr bytes.Buffer
|
|
||||||
cmd := exec.Command("ffmpeg",
|
|
||||||
"-hide_banner", "-loglevel", "error",
|
|
||||||
"-ss", fmt.Sprintf("%.3f", offset),
|
|
||||||
"-i", p.path,
|
|
||||||
"-vn",
|
|
||||||
"-ac", fmt.Sprintf("%d", channels),
|
|
||||||
"-ar", fmt.Sprintf("%d", sampleRate),
|
|
||||||
"-f", "s16le",
|
|
||||||
"-",
|
|
||||||
)
|
|
||||||
cmd.Stderr = &stderr
|
|
||||||
stdout, err := cmd.StdoutPipe()
|
|
||||||
if err != nil {
|
|
||||||
logging.Debug(logging.CatFFMPEG, "audio pipe error: %v", err)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
if err := cmd.Start(); err != nil {
|
|
||||||
errMsg := strings.TrimSpace(stderr.String())
|
|
||||||
fmt.Printf("❌ ERROR: Audio FFmpeg failed to start: %v\n", err)
|
|
||||||
if errMsg != "" {
|
|
||||||
fmt.Printf(" Audio FFmpeg error: %s\n", errMsg)
|
|
||||||
}
|
|
||||||
logging.Debug(logging.CatFFMPEG, "audio start failed: %v (%s)", err, errMsg)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
fmt.Printf("✅ Audio FFmpeg started (PID: %d)\n", cmd.Process.Pid)
|
|
||||||
p.audioCmd = cmd
|
|
||||||
ctx, err := getAudioContext(sampleRate, channels, bytesPerSample)
|
|
||||||
if err != nil {
|
|
||||||
logging.Debug(logging.CatFFMPEG, "audio context error: %v", err)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
player := ctx.NewPlayer()
|
|
||||||
if player == nil {
|
|
||||||
logging.Debug(logging.CatFFMPEG, "audio player creation failed")
|
|
||||||
return
|
|
||||||
}
|
|
||||||
localPlayer := player
|
|
||||||
go func() {
|
|
||||||
defer cmd.Process.Kill()
|
|
||||||
defer localPlayer.Close()
|
|
||||||
chunk := make([]byte, 4096)
|
|
||||||
tmp := make([]byte, 4096)
|
|
||||||
loggedFirst := false
|
|
||||||
for {
|
|
||||||
select {
|
|
||||||
case <-p.stop:
|
|
||||||
logging.Debug(logging.CatFFMPEG, "audio loop stop")
|
|
||||||
return
|
|
||||||
default:
|
|
||||||
}
|
|
||||||
if p.paused {
|
|
||||||
time.Sleep(30 * time.Millisecond)
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
n, err := stdout.Read(chunk)
|
|
||||||
if n > 0 {
|
|
||||||
if !loggedFirst {
|
|
||||||
logging.Debug(logging.CatFFMPEG, "audio stream delivering bytes")
|
|
||||||
loggedFirst = true
|
|
||||||
}
|
|
||||||
gain := p.volume / 100.0
|
|
||||||
if gain < 0 {
|
|
||||||
gain = 0
|
|
||||||
}
|
|
||||||
if gain > 2 {
|
|
||||||
gain = 2
|
|
||||||
}
|
|
||||||
copy(tmp, chunk[:n])
|
|
||||||
if p.muted || gain <= 0 {
|
|
||||||
for i := 0; i < n; i++ {
|
|
||||||
tmp[i] = 0
|
|
||||||
}
|
|
||||||
} else if math.Abs(1-gain) > 0.001 {
|
|
||||||
for i := 0; i+1 < n; i += 2 {
|
|
||||||
sample := int16(binary.LittleEndian.Uint16(tmp[i:]))
|
|
||||||
amp := int(float64(sample) * gain)
|
|
||||||
if amp > math.MaxInt16 {
|
|
||||||
amp = math.MaxInt16
|
|
||||||
}
|
|
||||||
if amp < math.MinInt16 {
|
|
||||||
amp = math.MinInt16
|
|
||||||
}
|
|
||||||
binary.LittleEndian.PutUint16(tmp[i:], uint16(int16(amp)))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
localPlayer.Write(tmp[:n])
|
|
||||||
}
|
|
||||||
if err != nil {
|
|
||||||
if !errors.Is(err, io.EOF) {
|
|
||||||
logging.Debug(logging.CatFFMPEG, "audio read failed: %v (%s)", err, strings.TrimSpace(stderr.String()))
|
|
||||||
}
|
|
||||||
return
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}()
|
}()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue
Block a user