163 lines
5.5 KiB
Go
163 lines
5.5 KiB
Go
package app
|
|
|
|
import (
|
|
"context"
|
|
"errors"
|
|
"net/http"
|
|
"net/url"
|
|
"strings"
|
|
"time"
|
|
)
|
|
|
|
func (app *UIApp) handleEmbyStream(writer http.ResponseWriter, request *http.Request) {
|
|
if !app.mediaResources().snapshot().Enabled {
|
|
app.handleEmbyLegacyStream(writer, request)
|
|
return
|
|
}
|
|
id, chapter, ok := app.authorizeEmby(writer, request)
|
|
if !ok {
|
|
return
|
|
}
|
|
if request.Method == http.MethodHead {
|
|
writer.Header().Set("Content-Type", "application/octet-stream")
|
|
return
|
|
}
|
|
owner := request.URL.Query().Get("account")
|
|
app.playbackMu.Lock()
|
|
var session *playbackSession
|
|
for _, candidate := range app.playbacks {
|
|
if candidate.viewer == nil && candidate.accountID == owner && candidate.mediaReady != nil && candidate.dramaID == id && len(candidate.tasks) == 1 && candidate.tasks[0].Chapter.ID == chapter && candidate.mediaError == nil && (candidate.media == nil || !candidate.media.failed.Load() && candidate.media.ctx.Err() == nil && (candidate.media.proxy == nil || candidate.media.proxy.Err() == nil)) {
|
|
session = candidate
|
|
break
|
|
}
|
|
}
|
|
if session == nil {
|
|
if len(app.playbacks) >= app.mediaResources().snapshot().MaxSessions {
|
|
app.playbackMu.Unlock()
|
|
writeJSON(writer, http.StatusTooManyRequests, map[string]string{"error": "同时播放数量已达上限,请稍后再试"})
|
|
return
|
|
}
|
|
sessionID := randomHex(24)
|
|
if sessionID == "" {
|
|
app.playbackMu.Unlock()
|
|
writer.WriteHeader(http.StatusInternalServerError)
|
|
return
|
|
}
|
|
parent, cancel := context.WithCancel(context.Background())
|
|
session = &playbackSession{id: sessionID, accountID: owner, dramaID: id, run: 1, state: "opening", mediaReady: make(chan struct{}), cancel: cancel,
|
|
tasks: []Task{{DramaID: id, Chapter: Chapter{ID: chapter}}}, expires: time.Now().Add(playbackIdleTimeout)}
|
|
if app.playbacks == nil {
|
|
app.playbacks = make(map[string]*playbackSession)
|
|
}
|
|
app.playbacks[sessionID] = session
|
|
session.timer = time.AfterFunc(playbackIdleTimeout, func() { app.expirePlayback(sessionID) })
|
|
query := url.Values{"id": {id}, "chapter": {chapter}, "key": {request.URL.Query().Get("key")}}
|
|
if owner != "" {
|
|
query.Set("account", owner)
|
|
}
|
|
origin := request.Header.Get("Origin")
|
|
go app.prepareEmbyMedia(parent, session, query.Encode(), origin)
|
|
}
|
|
session.mediaWaiters++
|
|
app.touchPlaybackLocked(session)
|
|
app.playbackMu.Unlock()
|
|
defer func() {
|
|
app.playbackMu.Lock()
|
|
session.mediaWaiters--
|
|
var cancel context.CancelFunc
|
|
if session.mediaWaiters == 0 && session.state == "opening" && app.playbacks[session.id] == session {
|
|
delete(app.playbacks, session.id)
|
|
session.timer.Stop()
|
|
cancel = session.cancel
|
|
}
|
|
app.playbackMu.Unlock()
|
|
if cancel != nil {
|
|
cancel()
|
|
}
|
|
}()
|
|
select {
|
|
case <-session.mediaReady:
|
|
case <-request.Context().Done():
|
|
return
|
|
}
|
|
app.playbackMu.Lock()
|
|
media, err := session.media, session.mediaError
|
|
app.playbackMu.Unlock()
|
|
if err != nil || media == nil {
|
|
if err == nil {
|
|
err = errors.New("播放会话已结束")
|
|
}
|
|
writeJSON(writer, http.StatusBadGateway, map[string]string{"error": "准备 Emby 播放失败:" + app.redactError(err)})
|
|
return
|
|
}
|
|
if media.plan.Player == "legacy" {
|
|
app.closePlayback(session.id)
|
|
app.handleEmbyLegacyStream(writer, request)
|
|
return
|
|
}
|
|
writer.Header().Set("Location", media.plan.URL)
|
|
writer.Header().Set("Cache-Control", "private, no-store")
|
|
writer.WriteHeader(http.StatusFound)
|
|
}
|
|
|
|
func (app *UIApp) prepareEmbyMedia(parent context.Context, session *playbackSession, query, origin string) {
|
|
ctx, cancel := context.WithTimeout(parent, 90*time.Second)
|
|
defer cancel()
|
|
task, downloadID, err := app.embyTask(ctx, session.dramaID, session.tasks[0].Chapter.ID)
|
|
var media *playbackMediaSession
|
|
if err == nil {
|
|
media, err = app.resolveMediaSession(parent, ctx, task, downloadID, 0)
|
|
}
|
|
if err == nil {
|
|
base := "/api/emby/media/" + session.id + "/"
|
|
err = app.planMedia(ctx, media, playbackClientCapabilities{MP4: true, NativeHLS: true}, "auto", sourceFromDramaID(task.DramaID), origin, base, query)
|
|
}
|
|
app.playbackMu.Lock()
|
|
if err == nil && (app.playbacks[session.id] != session || ctx.Err() != nil) {
|
|
err = context.Canceled
|
|
}
|
|
if err == nil {
|
|
previous := session.cancel
|
|
session.cancel = func() { previous(); media.Close() }
|
|
session.tasks, session.media, session.state = []Task{task}, media, "streaming"
|
|
session.duration = media.plan.Duration
|
|
media.plan.Run = session.run
|
|
app.touchPlaybackLocked(session)
|
|
} else {
|
|
session.mediaError, session.state = err, "failed"
|
|
}
|
|
close(session.mediaReady)
|
|
app.playbackMu.Unlock()
|
|
if err != nil {
|
|
if media != nil {
|
|
media.Close()
|
|
}
|
|
app.closePlayback(session.id)
|
|
}
|
|
}
|
|
|
|
func (app *UIApp) handleEmbyMediaAsset(writer http.ResponseWriter, request *http.Request) {
|
|
id, chapter, ok := app.authorizeEmby(writer, request)
|
|
if !ok {
|
|
return
|
|
}
|
|
parts := strings.Split(strings.TrimPrefix(request.URL.Path, "/api/emby/media/"), "/")
|
|
if len(parts) != 2 {
|
|
http.NotFound(writer, request)
|
|
return
|
|
}
|
|
app.playbackMu.Lock()
|
|
session := app.playbacks[parts[0]]
|
|
if session == nil || session.viewer != nil || session.media == nil || session.accountID != request.URL.Query().Get("account") || len(session.tasks) != 1 || session.tasks[0].DramaID != id || session.tasks[0].Chapter.ID != chapter {
|
|
app.playbackMu.Unlock()
|
|
writer.WriteHeader(http.StatusGone)
|
|
return
|
|
}
|
|
media := session.media
|
|
active := &playbackRunContext{id: session.id, run: session.run, session: session, ctx: media.ctx}
|
|
app.touchPlaybackLocked(session)
|
|
app.playbackMu.Unlock()
|
|
writer = &playbackActivityWriter{ResponseWriter: writer, app: app, run: active}
|
|
app.serveMediaSession(writer, request, media)
|
|
}
|