feat: add HLS manifest cache with TTL-based expiry
This commit is contained in:
109
internal/playback/handler/manifest_cache.go
Normal file
109
internal/playback/handler/manifest_cache.go
Normal file
@@ -0,0 +1,109 @@
|
||||
package handler
|
||||
|
||||
import (
|
||||
"container/list"
|
||||
"net/http"
|
||||
"strings"
|
||||
"sync"
|
||||
"time"
|
||||
)
|
||||
|
||||
const (
|
||||
manifestCacheVODTTL = 45 * time.Second
|
||||
manifestCacheLiveTTL = 3 * time.Second
|
||||
manifestCacheMaxEntries = 256
|
||||
manifestCacheMaxBodySize = 2 << 20
|
||||
)
|
||||
|
||||
type manifestCacheEntry struct {
|
||||
key string
|
||||
body []byte
|
||||
headers http.Header
|
||||
status int
|
||||
expiresAt time.Time
|
||||
}
|
||||
|
||||
type manifestCache struct {
|
||||
mu sync.Mutex
|
||||
maxEntries int
|
||||
entries map[string]*list.Element
|
||||
lru *list.List
|
||||
}
|
||||
|
||||
func newManifestCache(maxEntries int) *manifestCache {
|
||||
if maxEntries <= 0 {
|
||||
maxEntries = manifestCacheMaxEntries
|
||||
}
|
||||
return &manifestCache{
|
||||
maxEntries: maxEntries,
|
||||
entries: make(map[string]*list.Element, maxEntries),
|
||||
lru: list.New(),
|
||||
}
|
||||
}
|
||||
|
||||
func manifestCacheKey(targetURL string, referer string) string {
|
||||
return targetURL + "\x00" + referer
|
||||
}
|
||||
|
||||
func (c *manifestCache) get(key string, now time.Time) (manifestCacheEntry, bool) {
|
||||
c.mu.Lock()
|
||||
defer c.mu.Unlock()
|
||||
|
||||
el := c.entries[key]
|
||||
if el == nil {
|
||||
return manifestCacheEntry{}, false
|
||||
}
|
||||
entry, ok := el.Value.(manifestCacheEntry)
|
||||
if !ok || !now.Before(entry.expiresAt) {
|
||||
c.remove(el)
|
||||
return manifestCacheEntry{}, false
|
||||
}
|
||||
c.lru.MoveToFront(el)
|
||||
entry.body = append([]byte(nil), entry.body...)
|
||||
entry.headers = entry.headers.Clone()
|
||||
return entry, true
|
||||
}
|
||||
|
||||
func (c *manifestCache) set(key string, status int, headers http.Header, body []byte, now time.Time) bool {
|
||||
if status < http.StatusOK || status >= http.StatusMultipleChoices || len(body) > manifestCacheMaxBodySize {
|
||||
return false
|
||||
}
|
||||
|
||||
ttl := manifestCacheVODTTL
|
||||
bodyText := string(body)
|
||||
if strings.Contains(bodyText, "#EXTINF") && !strings.Contains(bodyText, "#EXT-X-ENDLIST") {
|
||||
ttl = manifestCacheLiveTTL
|
||||
}
|
||||
entry := manifestCacheEntry{
|
||||
key: key,
|
||||
body: append([]byte(nil), body...),
|
||||
headers: headers.Clone(),
|
||||
status: status,
|
||||
expiresAt: now.Add(ttl),
|
||||
}
|
||||
|
||||
c.mu.Lock()
|
||||
defer c.mu.Unlock()
|
||||
if el := c.entries[key]; el != nil {
|
||||
el.Value = entry
|
||||
c.lru.MoveToFront(el)
|
||||
return true
|
||||
}
|
||||
c.entries[key] = c.lru.PushFront(entry)
|
||||
for len(c.entries) > c.maxEntries {
|
||||
back := c.lru.Back()
|
||||
if back == nil {
|
||||
break
|
||||
}
|
||||
c.remove(back)
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
func (c *manifestCache) remove(el *list.Element) {
|
||||
entry, ok := el.Value.(manifestCacheEntry)
|
||||
if ok {
|
||||
delete(c.entries, entry.key)
|
||||
}
|
||||
c.lru.Remove(el)
|
||||
}
|
||||
Reference in New Issue
Block a user