feat(app): add internal components batch 6

This commit is contained in:
吴璨
2026-09-30 10:20:46 +08:00
parent 5bd126e026
commit baa1154cdd
10 changed files with 1731 additions and 0 deletions
+230
View File
@@ -0,0 +1,230 @@
package app
import (
"errors"
"fmt"
"io"
"net/http"
"strconv"
"strings"
"time"
)
type mediaByteRange struct {
start int64
end int64
total int64
}
func parseMediaByteRange(value string) (mediaByteRange, bool) {
var result mediaByteRange
unit, value, ok := strings.Cut(strings.TrimSpace(value), " ")
if !ok || !strings.EqualFold(unit, "bytes") {
return result, false
}
span, total, ok := strings.Cut(value, "/")
if !ok {
return result, false
}
start, end, ok := strings.Cut(span, "-")
if !ok {
return result, false
}
for index, field := range []string{start, end, total} {
if field == "" || strings.IndexFunc(field, func(char rune) bool { return char < '0' || char > '9' }) >= 0 {
return result, false
}
number, err := strconv.ParseInt(field, 10, 64)
if err != nil {
return result, false
}
switch index {
case 0:
result.start = number
case 1:
result.end = number
case 2:
result.total = number
}
}
return result, result.start <= result.end && result.end < result.total
}
func mediaRequestAttempts(retries int) int {
return min(3, max(1, retries))
}
func identityMediaResponse(response *http.Response) bool {
encoding := strings.TrimSpace(response.Header.Get("Content-Encoding"))
return !response.Uncompressed && (encoding == "" || strings.EqualFold(encoding, "identity")) &&
!strings.HasPrefix(strings.ToLower(response.Header.Get("Content-Type")), "multipart/")
}
type resumableMediaReader struct {
proxy *hlsProxy
request *http.Request
body io.ReadCloser
span mediaByteRange
length int64
read int64
etag string
modified string
validator string
resumable bool
attempts int
maxAttempts int
pending error
failure error
recovered bool
}
func (proxy *hlsProxy) mediaReader(request *http.Request, response *http.Response) *resumableMediaReader {
reader := &resumableMediaReader{
proxy: proxy, request: request.Clone(request.Context()), body: response.Body,
length: -1, maxAttempts: mediaRequestAttempts(proxy.retries) - 1,
etag: strings.TrimSpace(response.Header.Get("ETag")),
modified: strings.TrimSpace(response.Header.Get("Last-Modified")),
}
if response.Request != nil && response.Request.URL != nil {
reader.request.URL = response.Request.URL
}
if response.ContentLength > 0 {
reader.length = response.ContentLength
}
if response.StatusCode == http.StatusOK && reader.length > 0 && response.Header.Get("Content-Range") == "" {
reader.span = mediaByteRange{end: reader.length - 1, total: reader.length}
reader.resumable = true
} else if response.StatusCode == http.StatusPartialContent {
if span, valid := parseMediaByteRange(response.Header.Get("Content-Range")); valid {
length := span.end - span.start + 1
if response.ContentLength < 0 || response.ContentLength == length {
reader.span, reader.length, reader.resumable = span, length, true
}
}
}
reader.resumable = reader.resumable && request.Method == http.MethodGet && identityMediaResponse(response)
if len(reader.etag) >= 2 && strings.HasPrefix(reader.etag, `"`) && strings.HasSuffix(reader.etag, `"`) {
reader.validator = reader.etag
} else if _, err := http.ParseTime(reader.modified); err == nil {
reader.validator = reader.modified
}
return reader
}
func (reader *resumableMediaReader) Close() error {
return reader.body.Close()
}
func (reader *resumableMediaReader) Read(buffer []byte) (int, error) {
if len(buffer) == 0 {
return 0, nil
}
for {
if err := reader.request.Context().Err(); err != nil {
return 0, err
}
if reader.failure != nil {
return 0, reader.failure
}
if reader.length >= 0 && reader.read == reader.length {
if reader.attempts > 0 && !reader.recovered && reader.proxy.diagnostic != nil {
reader.recovered = true
reader.proxy.diagnostic(diagnosticEvent{Level: "info", Event: "media.recovered", Host: reader.request.URL.Hostname(),
OffsetBytes: reader.span.start + reader.read, TotalBytes: reader.span.total, Attempt: reader.attempts, Message: "媒体续读完成"})
}
return 0, io.EOF
}
if reader.pending != nil {
err := reader.pending
if errors.Is(err, io.EOF) {
if reader.length < 0 {
return 0, io.EOF
}
err = io.ErrUnexpectedEOF
}
if reader.resumable && reader.attempts < reader.maxAttempts {
if err = reader.resume(); err == nil {
reader.pending = nil
continue
}
}
if canceled := reader.request.Context().Err(); canceled != nil {
return 0, canceled
}
reader.failure = fmt.Errorf("媒体资源读取不完整(已读取 %d 字节,续读 %d 次): %w", reader.read, reader.attempts, err)
return 0, reader.failure
}
if reader.length >= 0 && int64(len(buffer)) > reader.length-reader.read {
buffer = buffer[:reader.length-reader.read]
}
count, err := reader.body.Read(buffer)
reader.read += int64(count)
reader.pending = err
if count > 0 || err == nil {
return count, nil
}
}
}
func (reader *resumableMediaReader) resume() error {
_ = reader.body.Close()
reader.body = http.NoBody
var lastErr error
for reader.attempts < reader.maxAttempts {
ctx := reader.request.Context()
if reader.attempts > 0 {
timer := time.NewTimer(time.Duration(reader.attempts) * 250 * time.Millisecond)
select {
case <-timer.C:
case <-ctx.Done():
timer.Stop()
return ctx.Err()
}
}
if err := ctx.Err(); err != nil {
return err
}
request := reader.request.Clone(ctx)
request.Header.Set("Range", fmt.Sprintf("bytes=%d-%d", reader.span.start+reader.read, reader.span.end))
request.Header.Del("If-Range")
if reader.validator != "" {
request.Header.Set("If-Range", reader.validator)
}
reader.attempts++
if reader.proxy.diagnostic != nil {
reader.proxy.diagnostic(diagnosticEvent{Level: "warn", Event: "media.resume", Host: reader.request.URL.Hostname(),
OffsetBytes: reader.span.start + reader.read, TotalBytes: reader.span.total, Attempt: reader.attempts, Message: "媒体连接中断,正在续读"})
}
response, err := reader.proxy.client.Do(request)
if err != nil {
lastErr = err
continue
}
if err = reader.validateContinuation(response); err == nil {
reader.body = response.Body
return nil
}
_ = response.Body.Close()
lastErr = err
if response.StatusCode != http.StatusRequestTimeout && response.StatusCode != http.StatusTooManyRequests && response.StatusCode < 500 {
return err
}
}
return lastErr
}
func (reader *resumableMediaReader) validateContinuation(response *http.Response) error {
if response.StatusCode != http.StatusPartialContent {
return fmt.Errorf("媒体续读未返回所需字节范围(HTTP %d)", response.StatusCode)
}
span, valid := parseMediaByteRange(response.Header.Get("Content-Range"))
if !valid || span.start != reader.span.start+reader.read || span.end != reader.span.end || span.total != reader.span.total ||
response.ContentLength >= 0 && response.ContentLength != reader.length-reader.read {
return errors.New("媒体续读字节范围或长度不一致")
}
if !identityMediaResponse(response) || reader.etag != "" && strings.TrimSpace(response.Header.Get("ETag")) != reader.etag ||
reader.modified != "" && strings.TrimSpace(response.Header.Get("Last-Modified")) != reader.modified {
return errors.New("媒体续读资源已变化")
}
return nil
}