112 lines
3.2 KiB
Go
112 lines
3.2 KiB
Go
package playback
|
|
|
|
import (
|
|
"context"
|
|
"fmt"
|
|
"io"
|
|
"log"
|
|
"net/http"
|
|
"strconv"
|
|
"strings"
|
|
"time"
|
|
)
|
|
|
|
func (s *Service) ProxyStream(ctx context.Context, targetURL string, referer string, rangeHeader string) (int, http.Header, []byte, io.ReadCloser, error) {
|
|
const maxRetries = 2
|
|
const retryDelay = 500 * time.Millisecond
|
|
|
|
var lastErr error
|
|
for attempt := 0; attempt <= maxRetries; attempt++ {
|
|
if attempt > 0 {
|
|
select {
|
|
case <-ctx.Done():
|
|
return 0, nil, nil, nil, ctx.Err()
|
|
case <-time.After(retryDelay):
|
|
}
|
|
log.Printf("retrying proxy request for %s (attempt %d/%d)", targetURL, attempt, maxRetries)
|
|
}
|
|
|
|
req, err := http.NewRequestWithContext(ctx, http.MethodGet, targetURL, nil)
|
|
if err != nil {
|
|
return 0, nil, nil, nil, fmt.Errorf("invalid upstream url: %w", err)
|
|
}
|
|
|
|
if referer != "" {
|
|
req.Header.Set("Referer", referer)
|
|
}
|
|
req.Header.Set("User-Agent", defaultUserAgent)
|
|
if rangeHeader != "" {
|
|
req.Header.Set("Range", rangeHeader)
|
|
}
|
|
|
|
resp, err := s.httpClient.Do(req)
|
|
if err != nil {
|
|
lastErr = err
|
|
continue
|
|
}
|
|
|
|
return s.handleProxyResponse(ctx, resp, targetURL, referer, rangeHeader)
|
|
}
|
|
|
|
return 0, nil, nil, nil, fmt.Errorf("upstream request failed after %d retries: %w", maxRetries+1, lastErr)
|
|
}
|
|
|
|
func (s *Service) handleProxyResponse(ctx context.Context, resp *http.Response, targetURL string, referer string, rangeHeader string) (int, http.Header, []byte, io.ReadCloser, error) {
|
|
|
|
if isM3U8(targetURL, resp.Header.Get("Content-Type")) {
|
|
defer resp.Body.Close()
|
|
body, readErr := io.ReadAll(io.LimitReader(resp.Body, 2*1024*1024))
|
|
if readErr != nil {
|
|
return 0, nil, nil, nil, fmt.Errorf("read playlist failed: %w", readErr)
|
|
}
|
|
|
|
rewritten, rewriteErr := s.rewritePlaylistWithTokens(ctx, string(body), targetURL, referer)
|
|
if rewriteErr != nil {
|
|
return 0, nil, nil, nil, fmt.Errorf("rewrite playlist failed: %w", rewriteErr)
|
|
}
|
|
|
|
headers := cloneHeaders(resp.Header)
|
|
headers.Del("Content-Length")
|
|
headers.Del("Transfer-Encoding")
|
|
headers.Set("Content-Type", "application/vnd.apple.mpegurl")
|
|
headers.Set("Content-Length", strconv.Itoa(len(rewritten)))
|
|
return resp.StatusCode, headers, []byte(rewritten), nil, nil
|
|
}
|
|
|
|
headers := cloneHeaders(resp.Header)
|
|
// Some upstream servers send transfer-encoding chunked, we should let go's http server handle it
|
|
headers.Del("Transfer-Encoding")
|
|
return resp.StatusCode, headers, nil, resp.Body, nil
|
|
}
|
|
|
|
func isM3U8(targetURL string, contentType string) bool {
|
|
if strings.Contains(strings.ToLower(targetURL), ".m3u8") {
|
|
return true
|
|
}
|
|
lowerType := strings.ToLower(contentType)
|
|
return strings.Contains(lowerType, "application/vnd.apple.mpegurl") || strings.Contains(lowerType, "application/x-mpegurl")
|
|
}
|
|
|
|
var hopHeaders = map[string]struct{}{
|
|
"connection": {},
|
|
"keep-alive": {},
|
|
"proxy-authenticate": {},
|
|
"proxy-authorization": {},
|
|
"te": {},
|
|
"trailers": {},
|
|
"upgrade": {},
|
|
}
|
|
|
|
func cloneHeaders(src http.Header) http.Header {
|
|
dst := make(http.Header)
|
|
for key, values := range src {
|
|
if _, ok := hopHeaders[strings.ToLower(key)]; ok {
|
|
continue
|
|
}
|
|
for _, value := range values {
|
|
dst.Add(key, value)
|
|
}
|
|
}
|
|
return dst
|
|
}
|