203 lines
6.0 KiB
Go
203 lines
6.0 KiB
Go
package app
|
|
|
|
import (
|
|
"context"
|
|
"encoding/json"
|
|
"errors"
|
|
"fmt"
|
|
"net/http"
|
|
"os"
|
|
"path/filepath"
|
|
"sync"
|
|
)
|
|
|
|
type playbackSettings struct {
|
|
Enabled bool `json:"enabled"`
|
|
DirectSources []string `json:"directSources"`
|
|
MaxSessions int `json:"maxSessions"`
|
|
MaxMediaRequests int `json:"maxMediaRequests"`
|
|
MaxRemuxJobs int `json:"maxRemuxJobs"`
|
|
MaxAudioTranscodes int `json:"maxAudioTranscodes"`
|
|
MaxVideoTranscodes int `json:"maxVideoTranscodes"`
|
|
MaxPrefetchJobs int `json:"maxPrefetchJobs"`
|
|
CacheMB int `json:"cacheMB"`
|
|
}
|
|
|
|
type playbackResources struct {
|
|
mu sync.Mutex
|
|
saveMu sync.Mutex
|
|
settings playbackSettings
|
|
active map[string]int
|
|
waiting map[string]int
|
|
changed chan struct{}
|
|
path string
|
|
cacheBytes int64
|
|
}
|
|
|
|
func defaultPlaybackSettings() playbackSettings {
|
|
return playbackSettings{Enabled: true, DirectSources: []string{}, MaxSessions: 4, MaxMediaRequests: 8,
|
|
MaxRemuxJobs: 2, MaxAudioTranscodes: 1, MaxVideoTranscodes: 1, MaxPrefetchJobs: 1, CacheMB: 512}
|
|
}
|
|
|
|
func (settings playbackSettings) validate() error {
|
|
if settings.MaxSessions < 1 || settings.MaxSessions > 64 || settings.MaxMediaRequests < 1 || settings.MaxMediaRequests > 64 ||
|
|
settings.MaxRemuxJobs < 0 || settings.MaxRemuxJobs > 8 || settings.MaxAudioTranscodes < 0 || settings.MaxAudioTranscodes > 8 ||
|
|
settings.MaxVideoTranscodes < 0 || settings.MaxVideoTranscodes > 8 || settings.MaxPrefetchJobs < 0 || settings.MaxPrefetchJobs > 4 ||
|
|
settings.CacheMB < 64 || settings.CacheMB > 8192 {
|
|
return errors.New("播放资源设置超出允许范围")
|
|
}
|
|
seen := make(map[string]bool)
|
|
for _, source := range settings.DirectSources {
|
|
valid := false
|
|
for _, choice := range accountSourceChoices {
|
|
valid = valid || source == choice.ID
|
|
}
|
|
if !valid || seen[source] {
|
|
return errors.New("直连站源选项无效")
|
|
}
|
|
seen[source] = true
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func (app *UIApp) mediaResources() *playbackResources {
|
|
app.playbackResourcesOnce.Do(func() {
|
|
resources := &playbackResources{settings: defaultPlaybackSettings(), active: make(map[string]int), waiting: make(map[string]int),
|
|
changed: make(chan struct{}), path: filepath.Join(app.cfg.dataDirectory(), "playback-settings.json")}
|
|
if body, err := os.ReadFile(resources.path); err == nil {
|
|
settings := defaultPlaybackSettings()
|
|
if len(body) <= 16384 && json.Unmarshal(body, &settings) == nil && settings.validate() == nil {
|
|
resources.settings = settings
|
|
}
|
|
}
|
|
app.playbackResources = resources
|
|
})
|
|
return app.playbackResources
|
|
}
|
|
|
|
func (resources *playbackResources) snapshot() playbackSettings {
|
|
resources.mu.Lock()
|
|
defer resources.mu.Unlock()
|
|
settings := resources.settings
|
|
settings.DirectSources = append([]string{}, settings.DirectSources...)
|
|
return settings
|
|
}
|
|
|
|
func (settings playbackSettings) limit(kind string) int {
|
|
switch kind {
|
|
case "media":
|
|
return settings.MaxMediaRequests
|
|
case "remux":
|
|
return settings.MaxRemuxJobs
|
|
case "audio":
|
|
return settings.MaxAudioTranscodes
|
|
case "video":
|
|
return settings.MaxVideoTranscodes
|
|
case "prefetch":
|
|
return settings.MaxPrefetchJobs
|
|
}
|
|
return 0
|
|
}
|
|
|
|
func (resources *playbackResources) notifyLocked() {
|
|
close(resources.changed)
|
|
resources.changed = make(chan struct{})
|
|
}
|
|
|
|
func (resources *playbackResources) acquire(ctx context.Context, kind string, optional bool) (func(), error) {
|
|
resources.mu.Lock()
|
|
if resources.waiting[kind] >= 128 {
|
|
resources.mu.Unlock()
|
|
return nil, errors.New("播放资源等待队列已满,请稍后重试")
|
|
}
|
|
resources.waiting[kind]++
|
|
defer func() {
|
|
resources.mu.Lock()
|
|
resources.waiting[kind]--
|
|
resources.notifyLocked()
|
|
resources.mu.Unlock()
|
|
}()
|
|
for {
|
|
limit := resources.settings.limit(kind)
|
|
if err := ctx.Err(); err != nil {
|
|
resources.mu.Unlock()
|
|
return nil, err
|
|
}
|
|
if limit == 0 {
|
|
resources.mu.Unlock()
|
|
return nil, fmt.Errorf("管理员已禁用此类播放处理(%s)", kind)
|
|
}
|
|
if resources.active[kind] < limit && (!optional || resources.waiting[kind] == 1) {
|
|
resources.active[kind]++
|
|
resources.mu.Unlock()
|
|
var once sync.Once
|
|
return func() {
|
|
once.Do(func() {
|
|
resources.mu.Lock()
|
|
resources.active[kind]--
|
|
resources.notifyLocked()
|
|
resources.mu.Unlock()
|
|
})
|
|
}, nil
|
|
}
|
|
if optional {
|
|
resources.mu.Unlock()
|
|
return nil, errors.New("前台播放正在使用资源,已跳过本次预缓存")
|
|
}
|
|
changed := resources.changed
|
|
resources.mu.Unlock()
|
|
select {
|
|
case <-changed:
|
|
case <-ctx.Done():
|
|
return nil, ctx.Err()
|
|
}
|
|
resources.mu.Lock()
|
|
}
|
|
}
|
|
|
|
func (app *UIApp) handlePlaybackSettings(writer http.ResponseWriter, request *http.Request) {
|
|
resources := app.mediaResources()
|
|
if request.Method == http.MethodGet {
|
|
writeJSON(writer, http.StatusOK, map[string]any{"settings": resources.snapshot(), "sources": accountSourceChoices})
|
|
return
|
|
}
|
|
var settings playbackSettings
|
|
if !readPlaybackRequest(writer, request, &settings) {
|
|
return
|
|
}
|
|
if err := settings.validate(); err != nil {
|
|
writeJSON(writer, http.StatusBadRequest, map[string]string{"error": err.Error()})
|
|
return
|
|
}
|
|
resources.saveMu.Lock()
|
|
defer resources.saveMu.Unlock()
|
|
body, _ := json.MarshalIndent(settings, "", " ")
|
|
err := os.MkdirAll(filepath.Dir(resources.path), 0700)
|
|
if err == nil {
|
|
var file *os.File
|
|
file, err = os.CreateTemp(filepath.Dir(resources.path), ".playback-settings-*")
|
|
if err == nil {
|
|
defer os.Remove(file.Name())
|
|
_, err = file.Write(body)
|
|
if syncErr := file.Sync(); err == nil {
|
|
err = syncErr
|
|
}
|
|
if closeErr := file.Close(); err == nil {
|
|
err = closeErr
|
|
}
|
|
if err == nil {
|
|
err = os.Rename(file.Name(), resources.path)
|
|
}
|
|
}
|
|
}
|
|
if err != nil {
|
|
writeJSON(writer, http.StatusInternalServerError, map[string]string{"error": "播放设置保存失败"})
|
|
return
|
|
}
|
|
resources.mu.Lock()
|
|
resources.settings = settings
|
|
resources.notifyLocked()
|
|
resources.mu.Unlock()
|
|
writeJSON(writer, http.StatusOK, map[string]any{"settings": settings, "sources": accountSourceChoices})
|
|
}
|