198 lines
5.1 KiB
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)
|
|
}
|
|
}
|