Files
djk/internal/app/playback_media_info.go

198 lines
5.1 KiB
Go

package app
import (
"context"
"encoding/binary"
"errors"
"fmt"
"io"
"net/http"
"os"
"strconv"
"strings"
"time"
)
type playbackMediaInfo struct {
Video string
Audio string
MIME string
Duration float64
MP4 bool
Range bool
}
func playbackMoovInfo(body []byte) playbackMediaInfo {
info := playbackMediaInfo{MP4: true}
mvhd := playbackMP4Child(body, "mvhd")
if len(mvhd) >= 20 && mvhd[0] == 0 {
scale := binary.BigEndian.Uint32(mvhd[12:16])
if scale > 0 {
info.Duration = float64(binary.BigEndian.Uint32(mvhd[16:20])) / float64(scale)
}
} else if len(mvhd) >= 32 && mvhd[0] == 1 {
scale := binary.BigEndian.Uint32(mvhd[20:24])
if scale > 0 {
info.Duration = float64(binary.BigEndian.Uint64(mvhd[24:32])) / float64(scale)
}
}
tracks, valid := playbackMP4Boxes(body)
if !valid {
return playbackMediaInfo{}
}
var codecs []string
for _, track := range tracks {
if track.kind != "trak" {
continue
}
stsd := playbackMP4Child(track.body, "mdia", "minf", "stbl", "stsd")
if len(stsd) < 8 {
continue
}
entries, ok := playbackMP4Boxes(stsd[8:])
if !ok || len(entries) != 1 {
continue
}
entry := entries[0]
kind := entry.kind
if kind == "encv" && len(entry.body) >= 78 {
kind = string(playbackMP4Child(entry.body[78:], "sinf", "frma"))
}
if kind == "enca" && len(entry.body) >= 28 {
kind = string(playbackMP4Child(entry.body[28:], "sinf", "frma"))
}
switch kind {
case "avc1", "avc3":
if len(entry.body) < 78 {
continue
}
avcc := playbackMP4Child(entry.body[78:], "avcC")
if len(avcc) < 4 {
continue
}
info.Video = "h264"
codecs = append(codecs, fmt.Sprintf("%s.%02X%02X%02X", kind, avcc[1], avcc[2], avcc[3]))
case "hvc1", "hev1":
info.Video = "hevc"
codecs = append(codecs, kind)
case "av01":
info.Video = "av1"
codecs = append(codecs, "av01")
case "vp09":
info.Video = "vp9"
codecs = append(codecs, "vp09")
case "mp4a":
if len(entry.body) >= 28 && playbackAACLC(playbackMP4Child(entry.body[28:], "esds")) {
info.Audio = "aac"
codecs = append(codecs, "mp4a.40.2")
} else {
info.Audio = "unknown"
}
case "ac-3":
info.Audio = "ac3"
codecs = append(codecs, kind)
case "ec-3":
info.Audio = "eac3"
codecs = append(codecs, kind)
case "Opus":
info.Audio = "opus"
codecs = append(codecs, "opus")
}
}
if info.Video != "" && info.Audio != "unknown" {
info.MIME = `video/mp4; codecs="` + strings.Join(codecs, ", ") + `"`
}
return info
}
func (app *UIApp) inspectPlaybackMedia(ctx context.Context, media *playbackMediaSession) {
if media.plan.Player == "hls" {
return
}
ctx, cancel := context.WithTimeout(ctx, 8*time.Second)
defer cancel()
var file *os.File
if media.local != "" {
var err error
file, err = os.Open(media.local)
if err != nil {
return
}
defer file.Close()
}
read := func(offset, length int64) ([]byte, error) {
if offset < 0 || length < 1 || length > 2<<20 {
return nil, errors.New("媒体索引读取范围无效")
}
if file != nil {
body := make([]byte, length)
_, err := file.ReadAt(body, offset)
media.info.Range = true
return body, err
}
release, err := app.mediaResources().acquire(ctx, "media", false)
if err != nil {
return nil, err
}
defer release()
request, err := http.NewRequestWithContext(ctx, http.MethodGet, media.media.URL, nil)
if err != nil {
return nil, err
}
request.Header.Set("Range", fmt.Sprintf("bytes=%d-%d", offset, offset+length-1))
request.Header.Set("Accept-Encoding", "identity")
request.Header.Set("User-Agent", userAgent)
request.Header.Set("Referer", media.media.Referer)
response, err := (&http.Client{Transport: app.downloader.client.Transport}).Do(request)
if err != nil {
return nil, err
}
defer response.Body.Close()
if response.StatusCode == http.StatusPartialContent {
if !strings.HasPrefix(response.Header.Get("Content-Range"), "bytes "+strconv.FormatInt(offset, 10)+"-") {
return nil, errors.New("媒体 Range 响应无效")
}
media.info.Range = true
} else if response.StatusCode != http.StatusOK || offset != 0 {
return nil, errors.New("媒体不支持索引范围读取")
}
body := make([]byte, length)
_, err = io.ReadFull(response.Body, body)
return body, err
}
var offset int64
for boxes := 0; boxes < 16; boxes++ {
header, err := read(offset, 16)
if err != nil {
return
}
size, headerSize := uint64(binary.BigEndian.Uint32(header)), uint64(8)
if size == 1 {
size, headerSize = binary.BigEndian.Uint64(header[8:16]), 16
}
if size < headerSize || size > 1<<40 {
return
}
if string(header[4:8]) == "ftyp" {
media.info.MP4 = true
}
if string(header[4:8]) == "moov" {
body, err := read(offset+int64(headerSize), int64(size-headerSize))
if err != nil {
return
}
info := playbackMoovInfo(body)
info.Range = media.info.Range
media.info = info
if info.Duration > 0 && info.Duration < 24*60*60 {
media.plan.Duration, media.plan.SeekEnd = info.Duration, info.Duration
}
if info.MIME != "" {
media.plan.MIME = info.MIME
}
return
}
offset += int64(size)
}
}