Pass embedded track language/title through play ACK into clients so Android/iOS can label tracks instead of generic Ondertitel N when ExoPlayer/VLC miss tags.
302 lines
5.9 KiB
Go
302 lines
5.9 KiB
Go
package mediainfo
|
|
|
|
import (
|
|
"encoding/binary"
|
|
"io"
|
|
"os"
|
|
"strings"
|
|
)
|
|
|
|
// Track is a media stream with optional Matroska language/title.
|
|
type Track struct {
|
|
Type string `json:"type"` // video | audio | subtitle | unknown
|
|
Number int `json:"number"`
|
|
Language string `json:"language,omitempty"`
|
|
Title string `json:"title,omitempty"`
|
|
Codec string `json:"codec,omitempty"`
|
|
}
|
|
|
|
// ProbeFile returns embedded tracks. For MKV/WebM we parse EBML; other containers return nil.
|
|
func ProbeFile(path string) ([]Track, error) {
|
|
lower := strings.ToLower(path)
|
|
if strings.HasSuffix(lower, ".mkv") || strings.HasSuffix(lower, ".webm") || strings.HasSuffix(lower, ".mka") {
|
|
return probeMatroska(path)
|
|
}
|
|
return nil, nil
|
|
}
|
|
|
|
func probeMatroska(path string) ([]Track, error) {
|
|
f, err := os.Open(path)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
defer f.Close()
|
|
|
|
info, err := f.Stat()
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
// Tracks element is normally near the start; cap read for huge files.
|
|
limit := info.Size()
|
|
if limit > 8<<20 {
|
|
limit = 8 << 20
|
|
}
|
|
r := &ebmlReader{r: io.LimitReader(f, limit)}
|
|
|
|
id, _, err := r.readIDSize()
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
// EBML header
|
|
if id != 0x1A45DFA3 {
|
|
return nil, nil
|
|
}
|
|
if err := r.skip(r.lastSize); err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
var tracks []Track
|
|
for {
|
|
id, size, err := r.readIDSize()
|
|
if err == io.EOF {
|
|
break
|
|
}
|
|
if err != nil {
|
|
break
|
|
}
|
|
switch id {
|
|
case 0x18538067: // Segment — descend
|
|
continue
|
|
case 0x1654AE6B: // Tracks
|
|
tracks, err = parseTracksElement(r, size)
|
|
return tracks, err
|
|
case 0x114D9B74, 0x1549A966, 0x1C53BB6B, 0x1F43B675, 0x1043A770, 0x1254C367, 0x1941A469:
|
|
// SeekHead, Info, Cues, Cluster, Chapters, Tags, Attachments
|
|
if err := r.skip(size); err != nil {
|
|
return tracks, nil
|
|
}
|
|
default:
|
|
if err := r.skip(size); err != nil {
|
|
return tracks, nil
|
|
}
|
|
}
|
|
}
|
|
return tracks, nil
|
|
}
|
|
|
|
func parseTracksElement(r *ebmlReader, size uint64) ([]Track, error) {
|
|
end := r.pos + size
|
|
var out []Track
|
|
for r.pos < end {
|
|
id, sz, err := r.readIDSize()
|
|
if err != nil {
|
|
return out, nil
|
|
}
|
|
if id == 0xAE { // TrackEntry
|
|
t, err := parseTrackEntry(r, sz)
|
|
if err == nil && t.Type != "" {
|
|
out = append(out, t)
|
|
}
|
|
} else {
|
|
_ = r.skip(sz)
|
|
}
|
|
}
|
|
return out, nil
|
|
}
|
|
|
|
func parseTrackEntry(r *ebmlReader, size uint64) (Track, error) {
|
|
end := r.pos + size
|
|
t := Track{Type: "unknown"}
|
|
for r.pos < end {
|
|
id, sz, err := r.readIDSize()
|
|
if err != nil {
|
|
return t, err
|
|
}
|
|
switch id {
|
|
case 0xD7: // TrackNumber
|
|
v, err := r.readUint(sz)
|
|
if err != nil {
|
|
return t, err
|
|
}
|
|
t.Number = int(v)
|
|
case 0x83: // TrackType
|
|
v, err := r.readUint(sz)
|
|
if err != nil {
|
|
return t, err
|
|
}
|
|
switch v {
|
|
case 1:
|
|
t.Type = "video"
|
|
case 2:
|
|
t.Type = "audio"
|
|
case 17:
|
|
t.Type = "subtitle"
|
|
default:
|
|
t.Type = "unknown"
|
|
}
|
|
case 0x22B59C, 0x22B59D: // Language / LanguageIETF
|
|
s, err := r.readString(sz)
|
|
if err != nil {
|
|
return t, err
|
|
}
|
|
s = strings.TrimSpace(s)
|
|
if s != "" && !strings.EqualFold(s, "und") {
|
|
t.Language = s
|
|
}
|
|
case 0x536E: // Name
|
|
s, err := r.readString(sz)
|
|
if err != nil {
|
|
return t, err
|
|
}
|
|
t.Title = strings.TrimSpace(s)
|
|
case 0x86: // CodecID
|
|
s, err := r.readString(sz)
|
|
if err != nil {
|
|
return t, err
|
|
}
|
|
t.Codec = s
|
|
default:
|
|
if err := r.skip(sz); err != nil {
|
|
return t, err
|
|
}
|
|
}
|
|
}
|
|
return t, nil
|
|
}
|
|
|
|
type ebmlReader struct {
|
|
r io.Reader
|
|
pos uint64
|
|
lastSize uint64
|
|
buf [8]byte
|
|
}
|
|
|
|
func (e *ebmlReader) readByte() (byte, error) {
|
|
n, err := e.r.Read(e.buf[:1])
|
|
if n == 1 {
|
|
e.pos++
|
|
return e.buf[0], nil
|
|
}
|
|
if err == nil {
|
|
err = io.EOF
|
|
}
|
|
return 0, err
|
|
}
|
|
|
|
func (e *ebmlReader) readIDSize() (id uint32, size uint64, err error) {
|
|
b0, err := e.readByte()
|
|
if err != nil {
|
|
return 0, 0, err
|
|
}
|
|
var idLen int
|
|
switch {
|
|
case b0&0x80 != 0:
|
|
idLen = 1
|
|
id = uint32(b0)
|
|
case b0&0x40 != 0:
|
|
idLen = 2
|
|
id = uint32(b0)
|
|
case b0&0x20 != 0:
|
|
idLen = 3
|
|
id = uint32(b0)
|
|
case b0&0x10 != 0:
|
|
idLen = 4
|
|
id = uint32(b0)
|
|
default:
|
|
return 0, 0, io.ErrUnexpectedEOF
|
|
}
|
|
for i := 1; i < idLen; i++ {
|
|
b, err := e.readByte()
|
|
if err != nil {
|
|
return 0, 0, err
|
|
}
|
|
id = (id << 8) | uint32(b)
|
|
}
|
|
|
|
b0, err = e.readByte()
|
|
if err != nil {
|
|
return 0, 0, err
|
|
}
|
|
var sizeLen int
|
|
var mask byte
|
|
switch {
|
|
case b0&0x80 != 0:
|
|
sizeLen, mask = 1, 0x7F
|
|
case b0&0x40 != 0:
|
|
sizeLen, mask = 2, 0x3F
|
|
case b0&0x20 != 0:
|
|
sizeLen, mask = 3, 0x1F
|
|
case b0&0x10 != 0:
|
|
sizeLen, mask = 4, 0x0F
|
|
case b0&0x08 != 0:
|
|
sizeLen, mask = 5, 0x07
|
|
case b0&0x04 != 0:
|
|
sizeLen, mask = 6, 0x03
|
|
case b0&0x02 != 0:
|
|
sizeLen, mask = 7, 0x01
|
|
case b0&0x01 != 0:
|
|
sizeLen, mask = 8, 0x00
|
|
default:
|
|
return 0, 0, io.ErrUnexpectedEOF
|
|
}
|
|
size = uint64(b0 & mask)
|
|
for i := 1; i < sizeLen; i++ {
|
|
b, err := e.readByte()
|
|
if err != nil {
|
|
return 0, 0, err
|
|
}
|
|
size = (size << 8) | uint64(b)
|
|
}
|
|
// Unknown size marker
|
|
if size == (uint64(1)<<(7*sizeLen))-1 {
|
|
size = 0
|
|
}
|
|
e.lastSize = size
|
|
return id, size, nil
|
|
}
|
|
|
|
func (e *ebmlReader) skip(n uint64) error {
|
|
if n == 0 {
|
|
return nil
|
|
}
|
|
buf := make([]byte, 32*1024)
|
|
left := n
|
|
for left > 0 {
|
|
chunk := uint64(len(buf))
|
|
if chunk > left {
|
|
chunk = left
|
|
}
|
|
read, err := io.ReadFull(e.r, buf[:chunk])
|
|
e.pos += uint64(read)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
left -= chunk
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func (e *ebmlReader) readUint(n uint64) (uint64, error) {
|
|
if n == 0 || n > 8 {
|
|
return 0, io.ErrUnexpectedEOF
|
|
}
|
|
var raw [8]byte
|
|
if _, err := io.ReadFull(e.r, raw[8-n:]); err != nil {
|
|
return 0, err
|
|
}
|
|
e.pos += n
|
|
return binary.BigEndian.Uint64(raw[:]), nil
|
|
}
|
|
|
|
func (e *ebmlReader) readString(n uint64) (string, error) {
|
|
if n > 1<<20 {
|
|
return "", io.ErrUnexpectedEOF
|
|
}
|
|
buf := make([]byte, n)
|
|
if _, err := io.ReadFull(e.r, buf); err != nil {
|
|
return "", err
|
|
}
|
|
e.pos += n
|
|
// Trim trailing NULs common in Matroska strings
|
|
return strings.TrimRight(string(buf), "\x00"), nil
|
|
}
|