mirror of
https://github.com/GopeedLab/gopeed.git
synced 2026-11-04 23:57:54 +00:00
249 lines
6.1 KiB
Go
249 lines
6.1 KiB
Go
package ffmpeg
|
|
|
|
import (
|
|
"context"
|
|
"errors"
|
|
"fmt"
|
|
"io"
|
|
"net/http"
|
|
"sync"
|
|
|
|
"github.com/GopeedLab/gopeed/internal/tempfiles"
|
|
)
|
|
|
|
// HTTPSpool downloads ahead independently of the FFmpeg semaphore. Range
|
|
// sources can fetch a missing block on demand, including blocks already evicted.
|
|
type HTTPSpool struct {
|
|
demand chan int64
|
|
demandDone chan struct{}
|
|
ctx context.Context
|
|
cancel context.CancelFunc
|
|
source Input
|
|
disk *DiskInput
|
|
network sync.Mutex
|
|
readMu sync.Mutex
|
|
pos int64
|
|
done chan struct{}
|
|
}
|
|
|
|
func OpenSpoolingHTTP(ctx context.Context, client *http.Client, source HTTPSource, root string, owners ...*tempfiles.Scope) (Input, error) {
|
|
ctx, cancel := context.WithCancel(ctx)
|
|
in, err := OpenHTTP(ctx, client, source)
|
|
if err != nil {
|
|
cancel()
|
|
return nil, err
|
|
}
|
|
disk, err := NewDiskInput(ctx, root, owners...)
|
|
if err != nil {
|
|
cancel()
|
|
in.Close()
|
|
return nil, err
|
|
}
|
|
if ranged := in.(*httpInput); ranged.ranged {
|
|
if _, err = disk.spool.WriteAt(ranged.block, 0); err != nil {
|
|
cancel()
|
|
in.Close()
|
|
disk.Close()
|
|
return nil, err
|
|
}
|
|
ranged.block = nil
|
|
}
|
|
h := &HTTPSpool{ctx: ctx, cancel: cancel, source: in, disk: disk, done: make(chan struct{}), demand: make(chan int64, 1), demandDone: make(chan struct{})}
|
|
go func() {
|
|
defer close(h.demandDone)
|
|
for {
|
|
select {
|
|
case offset := <-h.demand:
|
|
// A prefetch may have filled this position while demand was queued.
|
|
var probe [1]byte
|
|
if n, err, _ := h.disk.spool.readAt(probe[:], offset); n > 0 || err != nil {
|
|
continue
|
|
}
|
|
if err := h.fetch(offset / SpoolBlockSize * SpoolBlockSize); err != nil {
|
|
h.disk.End(err)
|
|
return
|
|
}
|
|
case <-ctx.Done():
|
|
return
|
|
}
|
|
}
|
|
}()
|
|
go func() {
|
|
defer close(h.done)
|
|
var err error
|
|
if !in.Seekable() {
|
|
_, err = io.CopyBuffer(disk, in, make([]byte, 64*1024))
|
|
} else {
|
|
for offset := int64(0); offset < in.Size(); offset += 3 * SpoolBlockSize {
|
|
if err = h.fetch(offset); err != nil {
|
|
break
|
|
}
|
|
}
|
|
}
|
|
disk.End(err)
|
|
}()
|
|
return h, nil
|
|
}
|
|
func (h *HTTPSpool) fetch(offset int64) error {
|
|
h.network.Lock()
|
|
defer h.network.Unlock()
|
|
if err := h.ctx.Err(); err != nil {
|
|
return err
|
|
}
|
|
source := h.source.(*httpInput)
|
|
end := offset + min(3*SpoolBlockSize, h.source.Size()-offset)
|
|
buffer := make([]byte, 64*1024)
|
|
for offset < end {
|
|
n, err, _ := h.disk.spool.readAt(buffer[:min(int64(len(buffer)), end-offset)], offset)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
if n > 0 {
|
|
offset += int64(n)
|
|
continue
|
|
}
|
|
resp, err := source.request(offset, end-1)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
err = func() error {
|
|
defer resp.Body.Close()
|
|
if resp.StatusCode != http.StatusPartialContent {
|
|
return fmt.Errorf("ffmpeg: Range request returned %d; source changed or stopped supporting Range", resp.StatusCode)
|
|
}
|
|
start, last, size, err := contentRange(resp.Header.Get("Content-Range"))
|
|
if err != nil || start != offset || last >= end || size != source.size {
|
|
return errors.New("ffmpeg: inconsistent Content-Range")
|
|
}
|
|
if source.etag != "" && resp.Header.Get("ETag") != source.etag {
|
|
return errors.New("ffmpeg: HTTP input ETag changed")
|
|
}
|
|
if source.etag == "" && source.modified != "" && resp.Header.Get("Last-Modified") != source.modified {
|
|
return errors.New("ffmpeg: HTTP input Last-Modified changed")
|
|
}
|
|
if err := identityEncoding(resp); err != nil {
|
|
return err
|
|
}
|
|
remaining := last - start + 1
|
|
if resp.ContentLength >= 0 && resp.ContentLength != remaining {
|
|
return errors.New("ffmpeg: Range response length mismatch")
|
|
}
|
|
for remaining > 0 {
|
|
n, err := resp.Body.Read(buffer[:min(int64(len(buffer)), remaining)])
|
|
if n > 0 {
|
|
if _, e := h.disk.spool.WriteAt(buffer[:n], offset); e != nil {
|
|
return e
|
|
}
|
|
offset += int64(n)
|
|
remaining -= int64(n)
|
|
}
|
|
if err != nil {
|
|
if err == io.EOF && remaining == 0 {
|
|
return nil
|
|
}
|
|
if err == io.EOF {
|
|
return io.ErrUnexpectedEOF
|
|
}
|
|
return err
|
|
}
|
|
if n == 0 {
|
|
return io.ErrNoProgress
|
|
}
|
|
}
|
|
n, err := resp.Body.Read(buffer[:1])
|
|
if n != 0 || err != io.EOF {
|
|
if err != nil && err != io.EOF {
|
|
return err
|
|
}
|
|
return errors.New("ffmpeg: oversized Range response")
|
|
}
|
|
return nil
|
|
}()
|
|
if err != nil {
|
|
return err
|
|
}
|
|
}
|
|
return nil
|
|
}
|
|
func (h *HTTPSpool) Read(p []byte) (int, error) {
|
|
if !h.Seekable() {
|
|
return h.disk.Read(p)
|
|
}
|
|
h.readMu.Lock()
|
|
defer h.readMu.Unlock()
|
|
if len(p) == 0 {
|
|
return 0, nil
|
|
}
|
|
if h.pos >= h.Size() {
|
|
if err := h.disk.spool.discard((h.Size() - 1) / SpoolBlockSize); err != nil {
|
|
return 0, err
|
|
}
|
|
return 0, io.EOF
|
|
}
|
|
for {
|
|
if err := h.ctx.Err(); err != nil {
|
|
return 0, err
|
|
}
|
|
n, err, wake := h.disk.spool.readAt(p, h.pos)
|
|
if n > 0 {
|
|
block := h.pos / SpoolBlockSize
|
|
h.pos += int64(n)
|
|
if h.pos/SpoolBlockSize > block {
|
|
if e := h.disk.spool.discard(block); err == nil {
|
|
err = e
|
|
}
|
|
}
|
|
return n, err
|
|
}
|
|
if err != nil {
|
|
return 0, err
|
|
}
|
|
select {
|
|
case h.demand <- h.pos:
|
|
default:
|
|
}
|
|
// Read as soon as a partial response supplies these bytes, rather than
|
|
// waiting for its entire multi-block Range request to finish.
|
|
select {
|
|
case <-wake:
|
|
case <-h.ctx.Done():
|
|
return 0, h.ctx.Err()
|
|
}
|
|
}
|
|
}
|
|
func (h *HTTPSpool) Seek(offset int64, whence int) (int64, error) {
|
|
if !h.Seekable() {
|
|
return 0, ErrSeek
|
|
}
|
|
h.readMu.Lock()
|
|
defer h.readMu.Unlock()
|
|
base := int64(0)
|
|
switch whence {
|
|
case io.SeekStart:
|
|
case io.SeekCurrent:
|
|
base = h.pos
|
|
case io.SeekEnd:
|
|
base = h.Size()
|
|
default:
|
|
return 0, errors.New("invalid seek origin")
|
|
}
|
|
next := base + offset
|
|
if next < 0 || (offset > 0 && next < base) {
|
|
return 0, errors.New("invalid seek offset")
|
|
}
|
|
h.pos = next
|
|
return next, nil
|
|
}
|
|
func (h *HTTPSpool) Size() int64 { return h.source.Size() }
|
|
func (h *HTTPSpool) Seekable() bool { return h.source.Seekable() }
|
|
func (h *HTTPSpool) Progress() (int64, int64) { return h.disk.Progress() }
|
|
func (h *HTTPSpool) Close() error {
|
|
h.cancel()
|
|
h.source.Close()
|
|
<-h.done
|
|
<-h.demandDone
|
|
return h.disk.Close()
|
|
}
|
|
|
|
func (h *HTTPSpool) Failures() <-chan error { return h.disk.Failures() }
|