Switch from mediainfo to ffprobe

This commit is contained in:
Zoe Roux 2024-06-09 19:50:09 +02:00
parent 32ccc54310
commit c11e1cc8f0
No known key found for this signature in database
7 changed files with 97 additions and 161 deletions

View File

@ -32,7 +32,6 @@ in
go
wgo
mediainfo
libmediainfo
ffmpeg-full
postgresql_15
pgformatter

View File

@ -20,7 +20,7 @@ ENV SSL_CERT_DIR=/etc/ssl/certs
RUN update-ca-certificates
RUN apt-get update \
&& apt-get install --no-install-recommends --no-install-suggests -y \
ffmpeg libavformat-dev libavutil-dev libswscale-dev libmediainfo-dev \
ffmpeg libavformat-dev libavutil-dev libswscale-dev \
&& apt-get clean autoclean -y \
&& apt-get autoremove -y
WORKDIR /app
@ -42,7 +42,7 @@ RUN sed -i -e's/ main/ main contrib non-free/g' /etc/apt/sources.list.d/debian.s
RUN set -x && apt-get update \
&& apt-get install --no-install-recommends --no-install-suggests -y \
# runtime dependencies
ffmpeg libmediainfo-dev \
ffmpeg \
# hwaccel dependencies
vainfo mesa-va-drivers \
# intel hwaccel dependencies, not available everywhere

View File

@ -31,9 +31,9 @@ RUN sed -i -e's/ main/ main contrib non-free/g' /etc/apt/sources.list.d/debian.s
RUN apt-get update \
&& apt-get install --no-install-recommends --no-install-suggests -y \
# runtime dependencies
ffmpeg libavformat-dev \
ffmpeg \
# build dependencies
libavutil-dev libswscale-dev libmediainfo-dev \
libavformat-dev libavutil-dev libswscale-dev \
# hwaccel dependencies
vainfo mesa-va-drivers \
# intel hwaccel dependencies, not available everywhere

View File

@ -12,13 +12,12 @@ require (
github.com/mattn/go-isatty v0.0.20 // indirect
github.com/valyala/bytebufferpool v1.0.0 // indirect
github.com/valyala/fasttemplate v1.2.2 // indirect
github.com/zoriya/go-mediainfo v0.0.0-20240113011752-07018f07efae
gitlab.com/opennota/screengen v1.0.2
golang.org/x/crypto v0.22.0 // indirect
golang.org/x/image v0.10.0 // indirect
golang.org/x/net v0.24.0 // indirect
golang.org/x/sys v0.19.0 // indirect
golang.org/x/text v0.14.0 // indirect
golang.org/x/time v0.5.0 // indirect
gopkg.in/vansante/go-ffprobe.v2 v2.2.0
)
require golang.org/x/image v0.10.0 // indirect

View File

@ -22,8 +22,6 @@ github.com/valyala/bytebufferpool v1.0.0/go.mod h1:6bBcMArwyJ5K/AmCkWv1jt77kVWyC
github.com/valyala/fasttemplate v1.2.2 h1:lxLXG0uE3Qnshl9QyaK6XJxMXlQZELvChBOCmQD0Loo=
github.com/valyala/fasttemplate v1.2.2/go.mod h1:KHLXt3tVN2HBp8eijSv/kGJopbvo7S+qRAEEKiv+SiQ=
github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY=
github.com/zoriya/go-mediainfo v0.0.0-20240113011752-07018f07efae h1:Xmtcb/GrG1jcLxC3cDFwdAM1oV7fLNBcN3h3YL+gN6g=
github.com/zoriya/go-mediainfo v0.0.0-20240113011752-07018f07efae/go.mod h1:jzun1oQGoJSh65g1XKaolTmjd6HW/34WHH7VMdJdbvM=
gitlab.com/opennota/screengen v1.0.2 h1:GxYTJdAPEzmg5v5CV4dgn45JVW+EcXXAvCxhE7w6UDw=
gitlab.com/opennota/screengen v1.0.2/go.mod h1:4kED4yriw2zslwYmXFCa5qCvEKwleBA7l5OE+d94NTU=
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
@ -71,5 +69,7 @@ golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtn
golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc=
golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU=
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
gopkg.in/vansante/go-ffprobe.v2 v2.2.0 h1:iuOqTsbfYuqIz4tAU9NWh22CmBGxlGHdgj4iqP+NUmY=
gopkg.in/vansante/go-ffprobe.v2 v2.2.0/go.mod h1:qF0AlAjk7Nqzqf3y333Ly+KxN3cKF2JqA3JT5ZheUGE=
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=

View File

@ -1,12 +1,11 @@
package src
import (
"cmp"
"fmt"
"log"
"strings"
"github.com/zoriya/go-mediainfo"
"gopkg.in/vansante/go-ffprobe.v2"
)
// convert mediainfo to RFC 6381, waiting for either of those tickets to be resolved:
@ -15,19 +14,13 @@ import (
// https://trac.ffmpeg.org/ticket/6617
//
// this code is addapted from https://github.com/jellyfin/jellyfin/blob/master/Jellyfin.Api/Helpers/HlsCodecStringHelpers.cs
func GetMimeCodec(mi *mediainfo.File, kind mediainfo.StreamKind, i int) *string {
codec := cmp.Or(
mi.Parameter(kind, i, "InternetMediaType"),
mi.Parameter(kind, i, "Format"),
)
switch codec {
case "video/H264", "AVC":
// and https://git.ffmpeg.org/gitweb/ffmpeg.git/blob/HEAD%3a/libavformat/hlsenc.c#l344
func GetMimeCodec(stream *ffprobe.Stream) *string {
switch stream.CodecName {
case "h264":
ret := "avc1"
info := strings.Split(strings.ToLower(mi.Parameter(kind, i, "Format_Profile")), "@")
format := info[0]
switch format {
switch strings.ToLower(stream.Profile) {
case "high":
ret += ".6400"
case "main":
@ -39,37 +32,30 @@ func GetMimeCodec(mi *mediainfo.File, kind mediainfo.StreamKind, i int) *string
ret += ".4240"
}
// level format is l3.1 for level 31
level := ParseFloat(info[1][1:])
ret += fmt.Sprintf("%02x", int(level*10))
ret += fmt.Sprintf("%02x", stream.Level)
return &ret
case "video/H265", "HEVC":
case "h265":
// The h265 syntax is a bit of a mystery at the time this comment was written.
// This is what I've found through various sources:
// FORMAT: [codecTag].[profile].[constraint?].L[level * 30].[UNKNOWN]
ret := "hvc1"
info := strings.Split(strings.ToLower(mi.Parameter(kind, i, "Format_Profile")), "@")
profile := info[0]
if profile == "main 10" {
if stream.Profile == "main 10" {
ret += ".2.4"
} else {
ret += ".1.4"
}
level := ParseFloat(info[1][:1])
ret += fmt.Sprintf(".L%02X.BO", int(level*30))
ret += fmt.Sprintf(".L%02X.BO", stream.Level)
return &ret
case "AV1":
case "av1":
// https://aomedia.org/av1/specification/annex-a/
// FORMAT: [codecTag].[profile].[level][tier].[bitDepth]
ret := "av01"
info := strings.Split(strings.ToLower(mi.Parameter(kind, i, "Format_Profile")), "@")
profile := info[0]
switch profile {
switch strings.ToLower(stream.Profile) {
case "main":
ret += ".0"
case "high":
@ -77,30 +63,24 @@ func GetMimeCodec(mi *mediainfo.File, kind mediainfo.StreamKind, i int) *string
case "professional":
ret += ".2"
default:
// Default to Main
ret += ".0"
}
// level is not defined in mediainfo. using a default
// Default to the maximum defined level 6.3
level := 19
bitdepth := ParseUint(mi.Parameter(kind, i, "BitDepth"))
// not sure about this field, we want pixel bit depth
bitdepth := ParseUint(stream.BitsPerRawSample)
if bitdepth != 8 && bitdepth != 10 && bitdepth != 12 {
// Default to 8 bits
bitdepth = 8
}
tierflag := 'M'
ret += fmt.Sprintf(".%02X%c.%02d", level, tierflag, bitdepth)
ret += fmt.Sprintf(".%02X%c.%02d", stream.Level, tierflag, bitdepth)
return &ret
case "AAC":
case "aac":
ret := "mp4a"
profile := strings.ToLower(mi.Parameter(kind, i, "Format_AdditionalFeatures"))
switch profile {
switch strings.ToLower(stream.Profile) {
case "he":
ret += ".40.5"
case "lc":
@ -111,24 +91,24 @@ func GetMimeCodec(mi *mediainfo.File, kind mediainfo.StreamKind, i int) *string
return &ret
case "audio/opus", "Opus":
case "opus":
ret := "Opus"
return &ret
case "AC-3":
case "ac-3":
ret := "mp4a.a5"
return &ret
case "audio/eac3", "E-AC-3":
case "eac3", "E-AC-3":
ret := "mp4a.a6"
return &ret
case "audio/x-flac", "FLAC":
case "x-flac", "FLAC":
ret := "fLaC"
return &ret
default:
log.Printf("No known mime format for: %s", codec)
log.Printf("No known mime format for: %s", stream.CodecName)
return nil
}
}

View File

@ -1,7 +1,7 @@
package src
import (
"cmp"
"context"
"encoding/base64"
"encoding/json"
"fmt"
@ -13,9 +13,9 @@ import (
"strconv"
"strings"
"sync"
"unicode"
"time"
"github.com/zoriya/go-mediainfo"
"gopkg.in/vansante/go-ffprobe.v2"
)
type MediaInfo struct {
@ -136,15 +136,6 @@ func ParseUint64(str string) uint64 {
return i
}
func ParseTime(str string) float32 {
x := strings.Split(str, ":")
hours, minutes, sms := ParseFloat(x[0]), ParseFloat(x[1]), x[2]
y := strings.Split(sms, ".")
seconds, ms := ParseFloat(y[0]), ParseFloat(y[1])
return (hours*60.+minutes)*60. + seconds + ms/1000.
}
func Map[T, U any](ts []T, f func(T, int) U) []U {
us := make([]U, len(ts))
for i := range ts {
@ -153,6 +144,25 @@ func Map[T, U any](ts []T, f func(T, int) U) []U {
return us
}
func MapStream[T any](streams []*ffprobe.Stream, kind ffprobe.StreamType, mapper func(*ffprobe.Stream, uint32) T) []T {
count := 0
for _, stream := range streams {
if stream.CodecType == string(kind) {
count++
}
}
ret := make([]T, count)
i := uint32(0)
for _, stream := range streams {
if stream.CodecType == string(kind) {
ret[i] = mapper(stream, i)
i++
}
}
return ret
}
func OrNull(str string) *string {
if str == "" {
return nil
@ -181,11 +191,11 @@ func GetInfo(path string, sha string) (*MediaInfo, error) {
mi.ready.Add(1)
go func() {
save_path := fmt.Sprintf("%s/%s/info.json", Settings.Metadata, sha)
if err := getSavedInfo(save_path, mi.info); err == nil {
// if err := getSavedInfo(save_path, mi.info); err == nil {
log.Printf("Using mediainfo cache on filesystem for %s", path)
mi.ready.Done()
return
}
// mi.ready.Done()
// return
// }
var val *MediaInfo
val, err = getInfo(path)
@ -229,65 +239,45 @@ func saveInfo[T any](save_path string, mi *T) error {
func getInfo(path string) (*MediaInfo, error) {
defer printExecTime("mediainfo for %s", path)()
mi, err := mediainfo.Open(path)
ctx, cancelFn := context.WithTimeout(context.Background(), 5*time.Second)
defer cancelFn()
mi, err := ffprobe.ProbeURL(ctx, path)
if err != nil {
return nil, err
}
defer mi.Close()
chapters_begin := ParseUint(mi.Parameter(mediainfo.StreamMenu, 0, "Chapters_Pos_Begin"))
chapters_end := ParseUint(mi.Parameter(mediainfo.StreamMenu, 0, "Chapters_Pos_End"))
attachments := strings.Split(mi.Parameter(mediainfo.StreamGeneral, 0, "Attachments"), " / ")
if len(attachments) == 1 && attachments[0] == "" {
attachments = make([]string, 0)
}
// fmt.Printf("%s", mi.Option("info_parameters", ""))
// duration in seconds
duration := ParseFloat(mi.Parameter(mediainfo.StreamGeneral, 0, "Duration")) / 1000
ret := MediaInfo{
Path: path,
// Remove leading .
Extension: filepath.Ext(path)[1:],
Size: ParseUint64(mi.Parameter(mediainfo.StreamGeneral, 0, "FileSize")),
Duration: duration,
Container: OrNull(mi.Parameter(mediainfo.StreamGeneral, 0, "Format")),
Videos: Map(make([]Video, ParseUint(mi.Parameter(mediainfo.StreamVideo, 0, "StreamCount"))), func(_ Video, i int) Video {
Size: ParseUint64(mi.Format.Size),
Duration: float32(mi.Format.DurationSeconds),
Container: OrNull(mi.Format.FormatName),
Videos: MapStream(mi.Streams, ffprobe.StreamVideo, func(stream *ffprobe.Stream, i uint32) Video {
return Video{
Codec: mi.Parameter(mediainfo.StreamVideo, i, "Format"),
MimeCodec: GetMimeCodec(mi, mediainfo.StreamVideo, i),
Language: OrNull(mi.Parameter(mediainfo.StreamVideo, i, "Language")),
Quality: QualityFromHeight(ParseUint(mi.Parameter(mediainfo.StreamVideo, i, "Height"))),
Width: ParseUint(mi.Parameter(mediainfo.StreamVideo, i, "Width")),
Height: ParseUint(mi.Parameter(mediainfo.StreamVideo, i, "Height")),
Bitrate: ParseUint(
cmp.Or(
mi.Parameter(mediainfo.StreamVideo, i, "BitRate"),
mi.Parameter(mediainfo.StreamVideo, i, "OverallBitRate"),
mi.Parameter(mediainfo.StreamVideo, i, "BitRate_Nominal"),
),
),
Codec: stream.CodecName,
MimeCodec: GetMimeCodec(stream),
Language: OrNull(stream.Tags.Language),
Quality: QualityFromHeight(uint32(stream.Height)),
Width: uint32(stream.Width),
Height: uint32(stream.Height),
Bitrate: ParseUint(stream.BitRate),
}
}),
Audios: Map(make([]Audio, ParseUint(mi.Parameter(mediainfo.StreamAudio, 0, "StreamCount"))), func(_ Audio, i int) Audio {
Audios: MapStream(mi.Streams, ffprobe.StreamAudio, func(stream *ffprobe.Stream, i uint32) Audio {
return Audio{
Index: uint32(i),
Title: OrNull(mi.Parameter(mediainfo.StreamAudio, i, "Title")),
Language: OrNull(mi.Parameter(mediainfo.StreamAudio, i, "Language")),
Codec: mi.Parameter(mediainfo.StreamAudio, i, "Format"),
MimeCodec: GetMimeCodec(mi, mediainfo.StreamAudio, i),
IsDefault: mi.Parameter(mediainfo.StreamAudio, i, "Default") == "Yes",
IsForced: mi.Parameter(mediainfo.StreamAudio, i, "Forced") == "Yes",
Index: i,
Title: OrNull(stream.Tags.Title),
Language: OrNull(stream.Tags.Language),
Codec: stream.CodecName,
MimeCodec: GetMimeCodec(stream),
IsDefault: stream.Disposition.Default != 0,
IsForced: stream.Disposition.Forced != 0,
}
}),
Subtitles: Map(make([]Subtitle, ParseUint(mi.Parameter(mediainfo.StreamText, 0, "StreamCount"))), func(_ Subtitle, i int) Subtitle {
format := strings.ToLower(mi.Parameter(mediainfo.StreamText, i, "Format"))
if format == "utf-8" {
format = "subrip"
}
extension := OrNull(SubtitleExtensions[format])
Subtitles: MapStream(mi.Streams, ffprobe.StreamSubtitle, func(stream *ffprobe.Stream, i uint32) Subtitle {
extension := OrNull(SubtitleExtensions[stream.CodecName])
var link *string
if extension != nil {
x := fmt.Sprintf("%s/%s/subtitle/%d.%s", Settings.RoutePrefix, base64.StdEncoding.EncodeToString([]byte(path)), i, *extension)
@ -295,21 +285,26 @@ func getInfo(path string) (*MediaInfo, error) {
}
return Subtitle{
Index: uint32(i),
Title: OrNull(mi.Parameter(mediainfo.StreamText, i, "Title")),
Language: OrNull(mi.Parameter(mediainfo.StreamText, i, "Language")),
Codec: format,
Title: OrNull(stream.Tags.Title),
Language: OrNull(stream.Tags.Language),
Codec: stream.CodecName,
Extension: extension,
IsDefault: mi.Parameter(mediainfo.StreamText, i, "Default") == "Yes",
IsForced: mi.Parameter(mediainfo.StreamText, i, "Forced") == "Yes",
IsDefault: stream.Disposition.Default != 0,
IsForced: stream.Disposition.Forced != 0,
Link: link,
}
}),
Chapters: getChapters(chapters_begin, chapters_end, mi, duration),
Fonts: Map(
attachments,
func(font string, _ int) string {
return fmt.Sprintf("%s/%s/attachment/%s", Settings.RoutePrefix, base64.StdEncoding.EncodeToString([]byte(path)), font)
}),
Chapters: Map(mi.Chapters, func(c *ffprobe.Chapter, _ int) Chapter {
return Chapter{
Name: c.Title(),
StartTime: float32(c.StartTimeSeconds),
EndTime: float32(c.EndTimeSeconds),
}
}),
Fonts: MapStream(mi.Streams, ffprobe.StreamAttachment, func(stream *ffprobe.Stream, i uint32) string {
font, _ := stream.TagList.GetString("filename")
return fmt.Sprintf("%s/%s/attachment/%s", Settings.RoutePrefix, base64.StdEncoding.EncodeToString([]byte(path)), font)
}),
}
var codecs []string
if len(ret.Videos) > 0 && ret.Videos[0].MimeCodec != nil {
@ -334,40 +329,3 @@ func getInfo(path string) (*MediaInfo, error) {
}
return &ret, nil
}
func chapterTimeIsValid(chapterTime string) bool {
return len(chapterTime) > 0 && unicode.IsDigit(rune(chapterTime[0]))
}
func getChapters(chapters_begin uint32, chapters_end uint32, mi *mediainfo.File, duration float32) []Chapter {
chapterCount := max(chapters_end-chapters_begin, 0)
chapterIterationCount := chapterCount
chapters := make([]Chapter, chapterCount)
chapterIndex := 0
for i := 0; i < int(chapterIterationCount); i++ {
rawStartTime := mi.GetI(mediainfo.StreamMenu, 0, int(chapters_begin)+i, mediainfo.InfoName)
rawEndTime := mi.GetI(mediainfo.StreamMenu, 0, int(chapters_begin)+i+1, mediainfo.InfoName)
// If true, this "chapter" is invalid. We skip it
if !chapterTimeIsValid(rawStartTime) {
chapterIterationCount = chapterIterationCount + 1
continue
}
var endTime float32
// If this fails, we probably are at the end of the video
// Since there would be no following chapter,
// we defacto set the end time to the end of the video (i.e. its duration)
if chapterTimeIsValid(rawEndTime) {
endTime = ParseTime(rawEndTime)
} else {
endTime = duration
}
chapters[chapterIndex] = Chapter{
StartTime: ParseTime(rawStartTime),
EndTime: endTime,
Name: mi.GetI(mediainfo.StreamMenu, 0, int(chapters_begin)+i, mediainfo.InfoText),
}
chapterIndex++
}
return chapters
}