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) }