初始提交:短剧库源码(含转码优化 ultrafast)
This commit is contained in:
@@ -0,0 +1,335 @@
|
||||
package app
|
||||
|
||||
import (
|
||||
"context"
|
||||
"net/http"
|
||||
"strconv"
|
||||
"sync"
|
||||
"time"
|
||||
)
|
||||
|
||||
const playbackPrefetchChunkSize = 64 * 1024
|
||||
const playbackPrefetchChunks = 512
|
||||
|
||||
type playbackPrefetchKey struct{}
|
||||
|
||||
type playbackPrefetch struct {
|
||||
episode int
|
||||
fromRun uint64
|
||||
quality int
|
||||
remux bool
|
||||
planOnly bool
|
||||
resolved *playbackMediaSession
|
||||
native *playbackNative
|
||||
ctx context.Context
|
||||
cancel context.CancelFunc
|
||||
chunks chan []byte
|
||||
ready chan struct{}
|
||||
done chan struct{}
|
||||
once sync.Once
|
||||
headers http.Header
|
||||
status int
|
||||
mu sync.Mutex
|
||||
header http.Header
|
||||
err error
|
||||
}
|
||||
|
||||
type playbackPrefetchView struct {
|
||||
Episode int `json:"episode"`
|
||||
State string `json:"state"`
|
||||
}
|
||||
|
||||
func newPlaybackPrefetch(episode int, run uint64) *playbackPrefetch {
|
||||
ctx := context.WithValue(context.Background(), backgroundCatalogKey{}, true)
|
||||
ctx = context.WithValue(ctx, playbackPrefetchKey{}, true)
|
||||
ctx, cancel := context.WithCancel(ctx)
|
||||
return &playbackPrefetch{episode: episode, fromRun: run, ctx: ctx, cancel: cancel,
|
||||
chunks: make(chan []byte, playbackPrefetchChunks), ready: make(chan struct{}), done: make(chan struct{}), headers: make(http.Header)}
|
||||
}
|
||||
|
||||
func (cache *playbackPrefetch) Header() http.Header { return cache.headers }
|
||||
func (cache *playbackPrefetch) WriteHeader(status int) { cache.status = status }
|
||||
func (cache *playbackPrefetch) Flush() {}
|
||||
func (cache *playbackPrefetch) SetWriteDeadline(time.Time) error { return nil }
|
||||
|
||||
func (cache *playbackPrefetch) Write(data []byte) (int, error) {
|
||||
if cache.status == 0 {
|
||||
cache.status = http.StatusOK
|
||||
}
|
||||
if cache.status != http.StatusOK {
|
||||
return len(data), nil
|
||||
}
|
||||
cache.once.Do(func() {
|
||||
cache.mu.Lock()
|
||||
cache.header = cache.headers.Clone()
|
||||
cache.mu.Unlock()
|
||||
close(cache.ready)
|
||||
})
|
||||
written := 0
|
||||
for len(data) > 0 {
|
||||
size := len(data)
|
||||
if size > playbackPrefetchChunkSize {
|
||||
size = playbackPrefetchChunkSize
|
||||
}
|
||||
chunk := append([]byte(nil), data[:size]...)
|
||||
select {
|
||||
case cache.chunks <- chunk:
|
||||
data = data[size:]
|
||||
written += size
|
||||
case <-cache.ctx.Done():
|
||||
return written, cache.ctx.Err()
|
||||
}
|
||||
}
|
||||
return written, nil
|
||||
}
|
||||
|
||||
func (cache *playbackPrefetch) finish(err error) {
|
||||
cache.mu.Lock()
|
||||
cache.err = err
|
||||
cache.mu.Unlock()
|
||||
close(cache.chunks)
|
||||
close(cache.done)
|
||||
cache.once.Do(func() { close(cache.ready) })
|
||||
}
|
||||
|
||||
func (cache *playbackPrefetch) view() *playbackPrefetchView {
|
||||
cache.mu.Lock()
|
||||
resolved := cache.resolved != nil && cache.err == nil
|
||||
cache.mu.Unlock()
|
||||
if resolved {
|
||||
return &playbackPrefetchView{Episode: cache.episode, State: "resolved"}
|
||||
}
|
||||
if cache.native != nil {
|
||||
state, _ := cache.native.state()
|
||||
switch state {
|
||||
case "ended":
|
||||
state = "ready"
|
||||
case "failed", "stopped":
|
||||
state = "failed"
|
||||
case "streaming":
|
||||
state = "buffering"
|
||||
select {
|
||||
case <-cache.native.firstBatch:
|
||||
state = "partial"
|
||||
default:
|
||||
}
|
||||
default:
|
||||
state = "preparing"
|
||||
}
|
||||
return &playbackPrefetchView{Episode: cache.episode, State: state}
|
||||
}
|
||||
cache.mu.Lock()
|
||||
defer cache.mu.Unlock()
|
||||
state := "preparing"
|
||||
if cache.err != nil {
|
||||
state = "failed"
|
||||
} else if cache.header != nil {
|
||||
state = "buffering"
|
||||
select {
|
||||
case <-cache.done:
|
||||
state = "ready"
|
||||
default:
|
||||
}
|
||||
}
|
||||
return &playbackPrefetchView{Episode: cache.episode, State: state}
|
||||
}
|
||||
|
||||
func (cache *playbackPrefetch) serve(ctx context.Context, writer http.ResponseWriter, run uint64, ready func(float64)) (bool, error) {
|
||||
select {
|
||||
case <-cache.ready:
|
||||
case <-ctx.Done():
|
||||
return true, ctx.Err()
|
||||
}
|
||||
cache.mu.Lock()
|
||||
header, failed := cache.header, cache.err
|
||||
cache.mu.Unlock()
|
||||
if failed != nil || header == nil {
|
||||
return false, nil
|
||||
}
|
||||
var first []byte
|
||||
select {
|
||||
case chunk, ok := <-cache.chunks:
|
||||
if !ok {
|
||||
return false, nil
|
||||
}
|
||||
first = chunk
|
||||
case <-ctx.Done():
|
||||
return true, ctx.Err()
|
||||
}
|
||||
for _, name := range []string{"Content-Type", "Content-Disposition", "X-Playback-Duration", "X-Playback-Source", "X-Playback-MIME", "X-Playback-Mode", "X-Playback-Quality", "X-Playback-Qualities"} {
|
||||
if value := header.Get(name); value != "" {
|
||||
writer.Header().Set(name, value)
|
||||
}
|
||||
}
|
||||
writer.Header().Set("X-Playback-Run", strconv.FormatUint(run, 10))
|
||||
writer.Header().Set("X-Playback-Prefetched", "1")
|
||||
duration, _ := strconv.ParseFloat(header.Get("X-Playback-Duration"), 64)
|
||||
ready(duration)
|
||||
controller := http.NewResponseController(writer)
|
||||
interrupted := make(chan struct{})
|
||||
stopInterrupt := context.AfterFunc(ctx, func() { _ = controller.SetWriteDeadline(time.Now()); close(interrupted) })
|
||||
defer func() {
|
||||
if !stopInterrupt() {
|
||||
<-interrupted
|
||||
}
|
||||
_ = controller.SetWriteDeadline(time.Time{})
|
||||
}()
|
||||
for {
|
||||
if _, err := writer.Write(first); err != nil {
|
||||
return true, err
|
||||
}
|
||||
if err := controller.Flush(); err != nil {
|
||||
return true, err
|
||||
}
|
||||
select {
|
||||
case chunk, ok := <-cache.chunks:
|
||||
if !ok {
|
||||
<-cache.done
|
||||
cache.mu.Lock()
|
||||
err := cache.err
|
||||
cache.mu.Unlock()
|
||||
return true, err
|
||||
}
|
||||
first = chunk
|
||||
case <-ctx.Done():
|
||||
return true, ctx.Err()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (app *UIApp) handlePlaybackPrefetch(writer http.ResponseWriter, request *http.Request) {
|
||||
var input struct {
|
||||
Session string `json:"session"`
|
||||
Episode int `json:"episode"`
|
||||
Run uint64 `json:"run"`
|
||||
Version uint64 `json:"version"`
|
||||
Cancel bool `json:"cancel"`
|
||||
Remux bool `json:"remux"`
|
||||
}
|
||||
if !readPlaybackRequest(writer, request, &input) {
|
||||
return
|
||||
}
|
||||
app.playbackMu.Lock()
|
||||
session := app.playbacks[input.Session]
|
||||
if !viewerOwnsPlayback(request.Context(), session) {
|
||||
app.playbackMu.Unlock()
|
||||
writeJSON(writer, http.StatusGone, map[string]string{"error": "播放会话已过期"})
|
||||
return
|
||||
}
|
||||
if input.Run != session.run || input.Version == 0 || input.Version < session.prefetchVersion {
|
||||
app.playbackMu.Unlock()
|
||||
writeJSON(writer, http.StatusConflict, map[string]string{"error": "播放分集已变化"})
|
||||
return
|
||||
}
|
||||
if !input.Cancel && (input.Episode < 1 || input.Episode != session.currentIndex+1 || input.Episode > len(session.tasks)) {
|
||||
app.playbackMu.Unlock()
|
||||
writeJSON(writer, http.StatusBadRequest, map[string]string{"error": "只能预缓存当前集的下一集"})
|
||||
return
|
||||
}
|
||||
state := session.state
|
||||
if session.native != nil {
|
||||
state, _ = session.native.state()
|
||||
}
|
||||
if !input.Cancel && state != "ended" && session.media == nil {
|
||||
app.playbackMu.Unlock()
|
||||
writeJSON(writer, http.StatusConflict, map[string]string{"error": "当前集尚未缓冲完成"})
|
||||
return
|
||||
}
|
||||
session.prefetchVersion = input.Version
|
||||
app.touchPlaybackLocked(session)
|
||||
previous := session.prefetch
|
||||
if input.Cancel {
|
||||
session.prefetch = nil
|
||||
app.playbackMu.Unlock()
|
||||
if previous != nil {
|
||||
previous.cancel()
|
||||
}
|
||||
writeJSON(writer, http.StatusOK, map[string]bool{"ok": true})
|
||||
return
|
||||
}
|
||||
remux := input.Remux && session.native == nil
|
||||
if previous != nil && previous.episode == input.Episode && previous.fromRun == input.Run && previous.remux == remux {
|
||||
view := previous.view()
|
||||
app.playbackMu.Unlock()
|
||||
writeJSON(writer, http.StatusOK, view)
|
||||
return
|
||||
}
|
||||
cache := newPlaybackPrefetch(input.Episode, input.Run)
|
||||
cache.quality, cache.remux = session.quality, remux
|
||||
cache.planOnly = session.media != nil
|
||||
cache.ctx = context.WithValue(cache.ctx, playbackQualityKey{}, cache.quality)
|
||||
cache.ctx = context.WithValue(cache.ctx, playbackRemuxKey{}, remux)
|
||||
session.prefetch = cache
|
||||
task := session.tasks[input.Episode-1]
|
||||
downloadID := ""
|
||||
if len(session.downloadIDs) > 0 {
|
||||
downloadID = session.downloadIDs[input.Episode-1]
|
||||
}
|
||||
if session.native != nil {
|
||||
cache.native = newPlaybackNative(app, cache.ctx, task, downloadID, cache.quality, 0, true)
|
||||
cancel := cache.cancel
|
||||
cache.cancel = func() { cancel(); cache.native.Close() }
|
||||
}
|
||||
app.playbackMu.Unlock()
|
||||
if previous != nil {
|
||||
previous.cancel()
|
||||
}
|
||||
go func() {
|
||||
release, err := app.mediaResources().acquire(cache.ctx, "prefetch", true)
|
||||
if err != nil {
|
||||
cache.cancel()
|
||||
cache.finish(err)
|
||||
return
|
||||
}
|
||||
defer release()
|
||||
startup := time.AfterFunc(60*time.Second, cache.cancel)
|
||||
defer startup.Stop()
|
||||
if cache.planOnly {
|
||||
defer cache.cancel()
|
||||
resolved, err := app.resolveMediaSession(cache.ctx, cache.ctx, task, downloadID, cache.quality)
|
||||
if err == nil {
|
||||
cache.mu.Lock()
|
||||
cache.resolved = resolved
|
||||
cache.mu.Unlock()
|
||||
resolved.Close()
|
||||
}
|
||||
cache.finish(err)
|
||||
return
|
||||
}
|
||||
if cache.native != nil {
|
||||
cache.native.start()
|
||||
_, err := cache.native.segment(cache.ctx, 0)
|
||||
startup.Stop()
|
||||
if err == nil {
|
||||
select {
|
||||
case <-cache.native.firstBatch:
|
||||
_, err = cache.native.state()
|
||||
case <-cache.ctx.Done():
|
||||
err = cache.ctx.Err()
|
||||
}
|
||||
}
|
||||
if err != nil {
|
||||
cache.cancel()
|
||||
}
|
||||
cache.finish(err)
|
||||
return
|
||||
}
|
||||
defer cache.cancel()
|
||||
err = app.streamPlayback(cache.ctx, cache.cancel, cache, task, downloadID, 0, 0, func(float64) { startup.Stop() })
|
||||
cache.finish(err)
|
||||
}()
|
||||
writeJSON(writer, http.StatusAccepted, &playbackPrefetchView{Episode: input.Episode, State: "preparing"})
|
||||
}
|
||||
|
||||
func (app *UIApp) servePrefetchedPlayback(ctx context.Context, writer http.ResponseWriter, cache *playbackPrefetch, run uint64, ready func(float64)) (bool, error) {
|
||||
if cache == nil {
|
||||
return false, nil
|
||||
}
|
||||
used, err := cache.serve(ctx, writer, run, ready)
|
||||
if !used {
|
||||
cache.cancel()
|
||||
}
|
||||
return used, err
|
||||
}
|
||||
|
||||
var _ http.ResponseWriter = (*playbackPrefetch)(nil)
|
||||
Reference in New Issue
Block a user