Git Repository

Mini Video Library

Mini Video Library scannt lokale Medienordner, verwaltet Metadaten und Vorschaubilder und streamt kompatible Videos direkt per HTTP an den Browser.

Projektseite ↗
HTTPShttps://zanvex.de/git/mini-video-library.git

internal/metadata/probe.go

Zum Verzeichnis
package metadata

import (
	"context"
	"encoding/json"
	"fmt"
	"os/exec"
	"strconv"
	"strings"
)

type Audio struct {
	Index           int `json:"index"`
	Language, Codec string
	Channels        int  `json:"channels"`
	Default         bool `json:"default"`
}
type Subtitle struct {
	Index           int `json:"index"`
	Language, Codec string
	Forced, Default bool
}
type Chapter struct {
	Start, End float64
	Title      string
}
type Result struct {
	Duration      float64
	VideoCodec    string
	Width, Height int
	FPS           float64
	Bitrate       int64
	Container     string
	HDR           bool
	Audio         []Audio
	Subtitles     []Subtitle
	Chapters      []Chapter
}
type raw struct {
	Format struct {
		Name     string `json:"format_name"`
		Duration string `json:"duration"`
		BitRate  string `json:"bit_rate"`
	} `json:"format"`
	Streams []struct {
		Index         int                           `json:"index"`
		CodecType     string                        `json:"codec_type"`
		CodecName     string                        `json:"codec_name"`
		Width         int                           `json:"width"`
		Height        int                           `json:"height"`
		Channels      int                           `json:"channels"`
		AvgFrameRate  string                        `json:"avg_frame_rate"`
		ColorTransfer string                        `json:"color_transfer"`
		Tags          map[string]string             `json:"tags"`
		Disposition   struct{ Default, Forced int } `json:"disposition"`
	} `json:"streams"`
	Chapters []struct {
		StartTime, EndTime string
		Tags               map[string]string
	} `json:"chapters"`
}

func Probe(ctx context.Context, binary, path string) (Result, error) {
	cmd := exec.CommandContext(ctx, binary, "-v", "error", "-show_format", "-show_streams", "-show_chapters", "-of", "json", path)
	b, e := cmd.CombinedOutput()
	if e != nil {
		message := strings.TrimSpace(string(b))
		if message == "" {
			message = "no diagnostic output"
		}
		return Result{}, fmt.Errorf("ffprobe %s: %w: %s", path, e, message)
	}
	var x raw
	if e = json.Unmarshal(b, &x); e != nil {
		return Result{}, e
	}
	r := Result{Container: strings.Split(x.Format.Name, ",")[0]}
	r.Duration, _ = strconv.ParseFloat(x.Format.Duration, 64)
	r.Bitrate, _ = strconv.ParseInt(x.Format.BitRate, 10, 64)
	for _, s := range x.Streams {
		lang := s.Tags["language"]
		switch s.CodecType {
		case "video":
			if r.VideoCodec == "" {
				r.VideoCodec = s.CodecName
				r.Width = s.Width
				r.Height = s.Height
				r.FPS = parseRate(s.AvgFrameRate)
				r.HDR = strings.Contains(strings.ToLower(s.ColorTransfer), "2084") || strings.Contains(strings.ToLower(s.ColorTransfer), "hlg")
			}
		case "audio":
			r.Audio = append(r.Audio, Audio{Index: s.Index, Language: lang, Codec: s.CodecName, Channels: s.Channels, Default: s.Disposition.Default == 1})
		case "subtitle":
			r.Subtitles = append(r.Subtitles, Subtitle{Index: s.Index, Language: lang, Codec: s.CodecName, Forced: s.Disposition.Forced == 1, Default: s.Disposition.Default == 1})
		}
	}
	for _, c := range x.Chapters {
		a, _ := strconv.ParseFloat(c.StartTime, 64)
		b, _ := strconv.ParseFloat(c.EndTime, 64)
		r.Chapters = append(r.Chapters, Chapter{a, b, c.Tags["title"]})
	}
	return r, nil
}
func parseRate(s string) float64 {
	p := strings.Split(s, "/")
	if len(p) != 2 {
		return 0
	}
	a, _ := strconv.ParseFloat(p[0], 64)
	b, _ := strconv.ParseFloat(p[1], 64)
	if b == 0 {
		return 0
	}
	return a / b
}