mirror of
https://github.com/GopeedLab/gopeed.git
synced 2026-11-07 09:00:47 +00:00
187 lines
3.8 KiB
Go
187 lines
3.8 KiB
Go
package ffmpeg
|
|
|
|
import (
|
|
"context"
|
|
"errors"
|
|
"io"
|
|
"os"
|
|
"sync"
|
|
"time"
|
|
)
|
|
|
|
// A bounded disk ring lets both JS inputs drain independently, including when
|
|
// they share a SABR producer. It does not make a sequential input seekable.
|
|
// A full ring applies backpressure. A prolonged stall fails instead of
|
|
// deadlocking a shared upstream or growing forever.
|
|
const StreamBufferLimit = 64 * 1024 * 1024
|
|
|
|
type StreamInput struct {
|
|
ctx context.Context
|
|
tempDir string
|
|
mu sync.Mutex
|
|
file *os.File
|
|
read, written int64
|
|
ended, closed bool
|
|
err error
|
|
wake chan struct{}
|
|
space chan struct{}
|
|
}
|
|
|
|
func NewStreamInput(ctx context.Context, tempDir string) *StreamInput {
|
|
return &StreamInput{ctx: ctx, tempDir: tempDir, wake: make(chan struct{}, 1), space: make(chan struct{}, 1)}
|
|
}
|
|
|
|
func (s *StreamInput) Write(p []byte) (int, error) {
|
|
s.mu.Lock()
|
|
defer s.mu.Unlock()
|
|
if len(p) > StreamBufferLimit {
|
|
return 0, errors.New("ffmpeg: input chunk exceeds buffer capacity")
|
|
}
|
|
var timer *time.Timer
|
|
defer func() {
|
|
if timer != nil {
|
|
timer.Stop()
|
|
}
|
|
}()
|
|
for int64(len(p)) > StreamBufferLimit-(s.written-s.read) && !s.closed && !s.ended {
|
|
if timer == nil {
|
|
timer = time.NewTimer(30 * time.Second)
|
|
}
|
|
s.mu.Unlock()
|
|
var err error
|
|
select {
|
|
case <-s.space:
|
|
case <-s.ctx.Done():
|
|
err = s.ctx.Err()
|
|
case <-timer.C:
|
|
err = errors.New("ffmpeg: input buffer stalled for 30 seconds; upstream tracks may be too far apart")
|
|
}
|
|
s.mu.Lock()
|
|
if err != nil {
|
|
return 0, err
|
|
}
|
|
}
|
|
if s.closed || s.ended {
|
|
return 0, io.ErrClosedPipe
|
|
}
|
|
if err := s.ctx.Err(); err != nil {
|
|
return 0, err
|
|
}
|
|
if len(p) == 0 {
|
|
return 0, nil
|
|
}
|
|
if s.file == nil {
|
|
// Android apps cannot write to Go's default /data/local/tmp.
|
|
if s.tempDir != "" {
|
|
if err := os.MkdirAll(s.tempDir, 0700); err != nil {
|
|
return 0, err
|
|
}
|
|
}
|
|
f, err := os.CreateTemp(s.tempDir, "gopeed-ffmpeg-*")
|
|
if err != nil {
|
|
return 0, err
|
|
}
|
|
s.file = f
|
|
}
|
|
off := s.written % StreamBufferLimit
|
|
first := min(len(p), StreamBufferLimit-int(off))
|
|
if _, err := s.file.WriteAt(p[:first], off); err != nil {
|
|
return 0, err
|
|
}
|
|
if first < len(p) {
|
|
if _, err := s.file.WriteAt(p[first:], 0); err != nil {
|
|
return 0, err
|
|
}
|
|
}
|
|
s.written += int64(len(p))
|
|
select {
|
|
case s.wake <- struct{}{}:
|
|
default:
|
|
}
|
|
return len(p), nil
|
|
}
|
|
|
|
func (s *StreamInput) End(err error) {
|
|
s.mu.Lock()
|
|
s.ended = true
|
|
if s.err == nil {
|
|
s.err = err
|
|
}
|
|
s.mu.Unlock()
|
|
select {
|
|
case s.space <- struct{}{}:
|
|
default:
|
|
}
|
|
select {
|
|
case s.wake <- struct{}{}:
|
|
default:
|
|
}
|
|
}
|
|
|
|
func (s *StreamInput) Read(p []byte) (int, error) {
|
|
if len(p) == 0 {
|
|
return 0, nil
|
|
}
|
|
for {
|
|
if err := s.ctx.Err(); err != nil {
|
|
return 0, err
|
|
}
|
|
s.mu.Lock()
|
|
if s.closed {
|
|
s.mu.Unlock()
|
|
return 0, io.ErrClosedPipe
|
|
}
|
|
if s.err != nil {
|
|
err := s.err
|
|
s.mu.Unlock()
|
|
return 0, err
|
|
}
|
|
if s.read < s.written {
|
|
off := s.read % StreamBufferLimit
|
|
n := min(int64(len(p)), s.written-s.read, StreamBufferLimit-off)
|
|
count, err := s.file.ReadAt(p[:n], off)
|
|
s.read += int64(count)
|
|
s.mu.Unlock()
|
|
select {
|
|
case s.space <- struct{}{}:
|
|
default:
|
|
}
|
|
return count, err
|
|
}
|
|
ended := s.ended
|
|
s.mu.Unlock()
|
|
if ended {
|
|
return 0, io.EOF
|
|
}
|
|
select {
|
|
case <-s.wake:
|
|
case <-s.ctx.Done():
|
|
return 0, s.ctx.Err()
|
|
}
|
|
}
|
|
}
|
|
|
|
func (s *StreamInput) Close() error {
|
|
s.mu.Lock()
|
|
defer s.mu.Unlock()
|
|
s.closed = true
|
|
select {
|
|
case s.space <- struct{}{}:
|
|
default:
|
|
}
|
|
select {
|
|
case s.wake <- struct{}{}:
|
|
default:
|
|
}
|
|
if s.file != nil {
|
|
name := s.file.Name()
|
|
s.file.Close()
|
|
s.file = nil
|
|
return os.Remove(name)
|
|
}
|
|
return nil
|
|
}
|
|
func (*StreamInput) Seek(int64, int) (int64, error) { return 0, ErrSeek }
|
|
func (*StreamInput) Size() int64 { return -1 }
|
|
func (*StreamInput) Seekable() bool { return false }
|