Suport multi video files

This commit is contained in:
Zoe Roux 2024-08-07 17:23:54 +02:00
parent 7d3c73a1e9
commit fa03d835ed
11 changed files with 146 additions and 100 deletions

View File

@ -74,7 +74,7 @@ func (h *Handler) GetVideoIndex(c echo.Context) error {
return err
}
ret, err := h.transcoder.GetVideoIndex(path, int32(video), quality, client, sha)
ret, err := h.transcoder.GetVideoIndex(path, uint32(video), quality, client, sha)
if err != nil {
return err
}
@ -102,7 +102,7 @@ func (h *Handler) GetAudioIndex(c echo.Context) error {
return err
}
ret, err := h.transcoder.GetAudioIndex(path, int32(audio), client, sha)
ret, err := h.transcoder.GetAudioIndex(path, uint32(audio), client, sha)
if err != nil {
return err
}
@ -138,7 +138,7 @@ func (h *Handler) GetVideoSegment(c echo.Context) error {
ret, err := h.transcoder.GetVideoSegment(
path,
int32(video),
uint32(video),
quality,
segment,
client,
@ -173,7 +173,7 @@ func (h *Handler) GetAudioSegment(c echo.Context) error {
return err
}
ret, err := h.transcoder.GetAudioSegment(path, int32(audio), segment, client, sha)
ret, err := h.transcoder.GetAudioSegment(path, uint32(audio), segment, client, sha)
if err != nil {
return err
}

View File

@ -7,10 +7,10 @@ import (
type AudioStream struct {
Stream
index int32
index uint32
}
func (t *Transcoder) NewAudioStream(file *FileStream, idx int32) (*AudioStream, error) {
func (t *Transcoder) NewAudioStream(file *FileStream, idx uint32) (*AudioStream, error) {
log.Printf("Creating a audio stream %d for %s", idx, file.Info.Path)
keyframes, err := t.metadataService.GetKeyframes(file.Info, false, idx)

View File

@ -16,11 +16,11 @@ type FileStream struct {
Out string
Info *MediaInfo
videos CMap[VideoKey, *VideoStream]
audios CMap[int32, *AudioStream]
audios CMap[uint32, *AudioStream]
}
type VideoKey struct {
idx int32
idx uint32
quality Quality
}
@ -29,7 +29,7 @@ func (t *Transcoder) newFileStream(path string, sha string) *FileStream {
transcoder: t,
Out: fmt.Sprintf("%s/%s", Settings.Outpath, sha),
videos: NewCMap[VideoKey, *VideoStream](),
audios: NewCMap[int32, *AudioStream](),
audios: NewCMap[uint32, *AudioStream](),
}
ret.ready.Add(1)
@ -67,51 +67,8 @@ func (fs *FileStream) Destroy() {
func (fs *FileStream) GetMaster() string {
master := "#EXTM3U\n"
if fs.Info.Video != nil {
var transmux_quality Quality
for _, quality := range Qualities {
if quality.Height() >= fs.Info.Video.Quality.Height() || quality.AverageBitrate() >= fs.Info.Video.Bitrate {
transmux_quality = quality
break
}
}
// original stream
{
bitrate := float64(fs.Info.Video.Bitrate)
master += "#EXT-X-STREAM-INF:"
master += fmt.Sprintf("AVERAGE-BANDWIDTH=%d,", int(math.Min(bitrate*0.8, float64(transmux_quality.AverageBitrate()))))
master += fmt.Sprintf("BANDWIDTH=%d,", int(math.Min(bitrate, float64(transmux_quality.MaxBitrate()))))
master += fmt.Sprintf("RESOLUTION=%dx%d,", fs.Info.Video.Width, fs.Info.Video.Height)
if fs.Info.Video.MimeCodec != nil {
master += fmt.Sprintf("CODECS=\"%s\",", *fs.Info.Video.MimeCodec)
}
master += "AUDIO=\"audio\","
master += "CLOSED-CAPTIONS=NONE\n"
master += fmt.Sprintf("./%s/index.m3u8\n", Original)
}
aspectRatio := float32(fs.Info.Video.Width) / float32(fs.Info.Video.Height)
// codec is the prefix + the level, the level is not part of the codec we want to compare for the same_codec check bellow
transmux_prefix := "avc1.6400"
transmux_codec := transmux_prefix + "28"
for _, quality := range Qualities {
same_codec := fs.Info.Video.MimeCodec != nil && strings.HasPrefix(*fs.Info.Video.MimeCodec, transmux_prefix)
inc_lvl := quality.Height() < fs.Info.Video.Quality.Height() ||
(quality.Height() == fs.Info.Video.Quality.Height() && !same_codec)
if inc_lvl {
master += "#EXT-X-STREAM-INF:"
master += fmt.Sprintf("AVERAGE-BANDWIDTH=%d,", quality.AverageBitrate())
master += fmt.Sprintf("BANDWIDTH=%d,", quality.MaxBitrate())
master += fmt.Sprintf("RESOLUTION=%dx%d,", int(aspectRatio*float32(quality.Height())+0.5), quality.Height())
master += fmt.Sprintf("CODECS=\"%s\",", transmux_codec)
master += "AUDIO=\"audio\","
master += "CLOSED-CAPTIONS=NONE\n"
master += fmt.Sprintf("./%s/index.m3u8\n", quality)
}
}
}
// TODO: support multiples audio qualities (and original)
for _, audio := range fs.Info.Audios {
master += "#EXT-X-MEDIA:TYPE=AUDIO,"
master += "GROUP-ID=\"audio\","
@ -128,12 +85,93 @@ func (fs *FileStream) GetMaster() string {
if audio.IsDefault {
master += "DEFAULT=YES,"
}
master += "CHANNELS=\"2\","
master += fmt.Sprintf("URI=\"./audio/%d/index.m3u8\"\n", audio.Index)
}
// codec is the prefix + the level, the level is not part of the codec we want to compare for the same_codec check bellow
transmux_prefix := "avc1.6400"
transmux_codec := transmux_prefix + "28"
audio_codec := "mp4a.40.2"
var def_video *Video
for _, video := range fs.Info.Videos {
if video.IsDefault {
def_video = &video
break
}
}
if def_video == nil && len(fs.Info.Videos) > 0 {
def_video = &fs.Info.Videos[0]
}
if def_video != nil {
qualities := Filter(Qualities, func(quality Quality) bool {
same_codec := def_video.MimeCodec != nil && strings.HasPrefix(*def_video.MimeCodec, transmux_prefix)
return quality.Height() < def_video.Quality().Height() ||
(quality.Height() == def_video.Quality().Height() && !same_codec)
})
for _, quality := range qualities {
for _, video := range fs.Info.Videos {
master += "#EXT-X-MEDIA:TYPE=VIDEO,"
master += fmt.Sprintf("GROUP-ID=\"%s\",", quality)
if video.Language != nil {
master += fmt.Sprintf("LANGUAGE=\"%s\",", *video.Language)
}
if video.Title != nil {
master += fmt.Sprintf("NAME=\"%s\",", *video.Title)
} else if video.Language != nil {
master += fmt.Sprintf("NAME=\"%s\",", *video.Language)
} else {
master += fmt.Sprintf("NAME=\"Video %d\",", video.Index)
}
if &video == def_video {
master += "DEFAULT=YES"
} else {
master += fmt.Sprintf("URI=\"./%d/%s/index.m3u8\"\n", video.Index, quality)
}
}
}
// original stream
{
bitrate := float64(def_video.Bitrate)
master += "#EXT-X-STREAM-INF:"
master += fmt.Sprintf("AVERAGE-BANDWIDTH=%d,", int(math.Min(bitrate*0.8, float64(def_video.Quality().AverageBitrate()))))
master += fmt.Sprintf("BANDWIDTH=%d,", int(math.Min(bitrate, float64(def_video.Quality().MaxBitrate()))))
master += fmt.Sprintf("RESOLUTION=%dx%d,", def_video.Width, def_video.Height)
if def_video.MimeCodec != nil {
master += fmt.Sprintf("CODECS=\"%s\",", strings.Join([]string{*def_video.MimeCodec, audio_codec}, ","))
}
master += "AUDIO=\"audio\","
master += "CLOSED-CAPTIONS=NONE\n"
master += fmt.Sprintf("./%d/%s/index.m3u8\n", def_video.Index, Original)
}
aspectRatio := float32(def_video.Width) / float32(def_video.Height)
for i, quality := range qualities {
if i == 0 {
// skip the original stream that already got handled
continue
}
master += "#EXT-X-STREAM-INF:"
master += fmt.Sprintf("AVERAGE-BANDWIDTH=%d,", quality.AverageBitrate())
master += fmt.Sprintf("BANDWIDTH=%d,", quality.MaxBitrate())
master += fmt.Sprintf("RESOLUTION=%dx%d,", int(aspectRatio*float32(quality.Height())+0.5), quality.Height())
master += fmt.Sprintf("CODECS=\"%s\",", strings.Join([]string{transmux_codec, audio_codec}, ","))
master += "AUDIO=\"audio\","
master += "CLOSED-CAPTIONS=NONE\n"
master += fmt.Sprintf("./%s/index.m3u8\n", quality)
}
}
return master
}
func (fs *FileStream) getVideoStream(idx int32, quality Quality) (*VideoStream, error) {
func (fs *FileStream) getVideoStream(idx uint32, quality Quality) (*VideoStream, error) {
var err error
stream, _ := fs.videos.GetOrCreate(VideoKey{idx, quality}, func() *VideoStream {
var ret *VideoStream
@ -148,7 +186,7 @@ func (fs *FileStream) getVideoStream(idx int32, quality Quality) (*VideoStream,
return stream, nil
}
func (fs *FileStream) GetVideoIndex(idx int32, quality Quality) (string, error) {
func (fs *FileStream) GetVideoIndex(idx uint32, quality Quality) (string, error) {
stream, err := fs.getVideoStream(idx, quality)
if err != nil {
return "", err
@ -156,7 +194,7 @@ func (fs *FileStream) GetVideoIndex(idx int32, quality Quality) (string, error)
return stream.GetIndex()
}
func (fs *FileStream) GetVideoSegment(idx int32, quality Quality, segment int32) (string, error) {
func (fs *FileStream) GetVideoSegment(idx uint32, quality Quality, segment int32) (string, error) {
stream, err := fs.getVideoStream(idx, quality)
if err != nil {
return "", err
@ -164,7 +202,7 @@ func (fs *FileStream) GetVideoSegment(idx int32, quality Quality, segment int32)
return stream.GetSegment(segment)
}
func (fs *FileStream) getAudioStream(audio int32) (*AudioStream, error) {
func (fs *FileStream) getAudioStream(audio uint32) (*AudioStream, error) {
var err error
stream, _ := fs.audios.GetOrCreate(audio, func() *AudioStream {
var ret *AudioStream
@ -179,7 +217,7 @@ func (fs *FileStream) getAudioStream(audio int32) (*AudioStream, error) {
return stream, nil
}
func (fs *FileStream) GetAudioIndex(audio int32) (string, error) {
func (fs *FileStream) GetAudioIndex(audio uint32) (string, error) {
stream, err := fs.getAudioStream(audio)
if err != nil {
return "", nil
@ -187,7 +225,7 @@ func (fs *FileStream) GetAudioIndex(audio int32) (string, error) {
return stream.GetIndex()
}
func (fs *FileStream) GetAudioSegment(audio int32, segment int32) (string, error) {
func (fs *FileStream) GetAudioSegment(audio uint32, segment int32) (string, error) {
stream, err := fs.getAudioStream(audio)
if err != nil {
return "", nil

View File

@ -65,8 +65,6 @@ type Video struct {
Codec string `json:"codec"`
/// The codec of this stream (defined as the RFC 6381).
MimeCodec *string `json:"mimeCodec"`
/// The max quality of this video track.
Quality Quality `json:"quality"`
/// The width of the video stream
Width uint32 `json:"width"`
/// The height of the video stream
@ -251,12 +249,11 @@ func RetriveMediaInfo(path string, sha string) (*MediaInfo, error) {
MimeCodec: GetMimeCodec(stream),
Title: OrNull(stream.Tags.Title),
Language: NullIfUnd(lang.String()),
Quality: QualityFromHeight(uint32(stream.Height)),
Width: uint32(stream.Width),
Height: uint32(stream.Height),
// ffmpeg does not report bitrate in mkv files, fallback to bitrate of the whole container
// (bigger than the result since it contains audio and other videos but better than nothing).
Bitrate: ParseUint(cmp.Or(stream.BitRate, mi.Format.BitRate)),
Bitrate: ParseUint(cmp.Or(stream.BitRate, mi.Format.BitRate)),
IsDefault: stream.Disposition.Default != 0,
}
}),
@ -268,7 +265,7 @@ func RetriveMediaInfo(path string, sha string) (*MediaInfo, error) {
Language: NullIfUnd(lang.String()),
Codec: stream.CodecName,
MimeCodec: GetMimeCodec(stream),
Bitrate: ParseUint(cmp.Or(stream.BitRate, mi.Format.BitRate)),
Bitrate: ParseUint(cmp.Or(stream.BitRate, mi.Format.BitRate)),
IsDefault: stream.Disposition.Default != 0,
}
}),
@ -325,9 +322,5 @@ func RetriveMediaInfo(path string, sha string) (*MediaInfo, error) {
ret.MimeCodec = &container
}
}
if len(ret.Videos) > 0 {
ret.Video = &ret.Videos[0]
}
return &ret, nil
}

View File

@ -83,10 +83,10 @@ func (kf *Keyframe) Scan(src interface{}) error {
type KeyframeKey struct {
Sha string
IsVideo bool
Index int32
Index uint32
}
func (s *MetadataService) GetKeyframes(info *MediaInfo, isVideo bool, idx int32) (*Keyframe, error) {
func (s *MetadataService) GetKeyframes(info *MediaInfo, isVideo bool, idx uint32) (*Keyframe, error) {
get_running, set := s.keyframeLock.Start(KeyframeKey{
Sha: info.Sha,
IsVideo: isVideo,
@ -138,7 +138,7 @@ func (s *MetadataService) GetKeyframes(info *MediaInfo, isVideo bool, idx int32)
// Retrive video's keyframes and store them inside the kf var.
// Returns when all key frames are retrived (or an error occurs)
// info.ready.Done() is called when more than 100 are retrived (or extraction is done)
func getVideoKeyframes(path string, video_idx int32, kf *Keyframe) error {
func getVideoKeyframes(path string, video_idx uint32, kf *Keyframe) error {
defer printExecTime("ffprobe keyframe analysis for %s video n%d", path, video_idx)()
// run ffprobe to return all IFrames, IFrames are points where we can split the video in segments.
// We ask ffprobe to return the time of each frame and it's flags
@ -224,7 +224,7 @@ func getVideoKeyframes(path string, video_idx int32, kf *Keyframe) error {
}
// we can pretty much cut audio at any point so no need to get specific frames, just cut every 4s
func getAudioKeyframes(info *MediaInfo, audio_idx int32, kf *Keyframe) error {
func getAudioKeyframes(info *MediaInfo, audio_idx uint32, kf *Keyframe) error {
dummyKeyframeDuration := float64(4)
segmentCount := int((float64(info.Duration) / dummyKeyframeDuration) + 1)
kf.Keyframes = make([]float64, segmentCount)

View File

@ -130,7 +130,6 @@ func (s *MetadataService) getMetadata(path string, sha string) (*MediaInfo, erro
if err != nil {
return nil, err
}
v.Quality = QualityFromHeight(v.Height)
ret.Videos = append(ret.Videos, v)
}

View File

@ -110,10 +110,10 @@ func (q Quality) Height() uint32 {
panic("Invalid quality value")
}
func QualityFromHeight(height uint32) Quality {
func (video *Video) Quality() Quality {
qualities := Qualities
for _, quality := range qualities {
if quality.Height() >= height {
if quality.Height() >= video.Height || quality.AverageBitrate() >= video.Bitrate {
return quality
}
}

View File

@ -9,7 +9,7 @@ type ClientInfo struct {
client string
path string
video *VideoKey
audio int32
audio *uint32
vhead int32
ahead int32
}
@ -60,7 +60,7 @@ func (t *Tracker) start() {
if info.video == nil {
info.video = old.video
}
if info.audio == -1 {
if info.audio == nil {
info.audio = old.audio
}
if info.vhead == -1 {
@ -77,14 +77,14 @@ func (t *Tracker) start() {
// now that the new info is stored and fixed, kill old streams
if ok && old.path == info.path {
if old.audio != info.audio && old.audio != -1 {
t.KillAudioIfDead(old.path, old.audio)
if old.audio != info.audio && old.audio != nil {
t.KillAudioIfDead(old.path, *old.audio)
}
if old.video != info.video && old.video != nil {
t.KillVideoIfDead(old.path, *old.video)
}
if old.vhead != -1 && Abs(info.vhead-old.vhead) > 100 {
t.KillOrphanedHeads(old.path, old.video, -1)
t.KillOrphanedHeads(old.path, old.video, nil)
}
if old.ahead != -1 && Abs(info.ahead-old.ahead) > 100 {
t.KillOrphanedHeads(old.path, nil, old.audio)
@ -106,7 +106,7 @@ func (t *Tracker) start() {
delete(t.visitDate, client)
if !t.KillStreamIfDead(info.path) {
audio_cleanup := info.audio != -1 && t.KillAudioIfDead(info.path, info.audio)
audio_cleanup := info.audio != nil && t.KillAudioIfDead(info.path, *info.audio)
video_cleanup := info.video != nil && t.KillVideoIfDead(info.path, *info.video)
if !audio_cleanup || !video_cleanup {
t.KillOrphanedHeads(info.path, info.video, info.audio)
@ -150,9 +150,9 @@ func (t *Tracker) DestroyStreamIfOld(path string) {
stream.Destroy()
}
func (t *Tracker) KillAudioIfDead(path string, audio int32) bool {
func (t *Tracker) KillAudioIfDead(path string, audio uint32) bool {
for _, stream := range t.clients {
if stream.path == path && stream.audio == audio {
if stream.path == path && stream.audio != nil && *stream.audio == audio {
return false
}
}
@ -190,7 +190,7 @@ func (t *Tracker) KillVideoIfDead(path string, video VideoKey) bool {
return true
}
func (t *Tracker) KillOrphanedHeads(path string, video *VideoKey, audio int32) {
func (t *Tracker) KillOrphanedHeads(path string, video *VideoKey, audio *uint32) {
stream, ok := t.transcoder.streams.Get(path)
if !ok {
return
@ -202,8 +202,8 @@ func (t *Tracker) KillOrphanedHeads(path string, video *VideoKey, audio int32) {
t.killOrphanedeheads(&vstream.Stream, true)
}
}
if audio != -1 {
astream, aok := stream.audios.Get(audio)
if audio != nil {
astream, aok := stream.audios.Get(*audio)
if aok {
t.killOrphanedeheads(&astream.Stream, false)
}

View File

@ -56,7 +56,7 @@ func (t *Transcoder) GetMaster(path string, client string, sha string) (string,
client: client,
path: path,
video: nil,
audio: -1,
audio: nil,
vhead: -1,
ahead: -1,
}
@ -65,7 +65,7 @@ func (t *Transcoder) GetMaster(path string, client string, sha string) (string,
func (t *Transcoder) GetVideoIndex(
path string,
video int32,
video uint32,
quality Quality,
client string,
sha string,
@ -78,7 +78,7 @@ func (t *Transcoder) GetVideoIndex(
client: client,
path: path,
video: &VideoKey{video, quality},
audio: -1,
audio: nil,
vhead: -1,
ahead: -1,
}
@ -87,7 +87,7 @@ func (t *Transcoder) GetVideoIndex(
func (t *Transcoder) GetAudioIndex(
path string,
audio int32,
audio uint32,
client string,
sha string,
) (string, error) {
@ -98,7 +98,7 @@ func (t *Transcoder) GetAudioIndex(
t.clientChan <- ClientInfo{
client: client,
path: path,
audio: audio,
audio: &audio,
vhead: -1,
ahead: -1,
}
@ -107,7 +107,7 @@ func (t *Transcoder) GetAudioIndex(
func (t *Transcoder) GetVideoSegment(
path string,
video int32,
video uint32,
quality Quality,
segment int32,
client string,
@ -122,7 +122,7 @@ func (t *Transcoder) GetVideoSegment(
path: path,
video: &VideoKey{video, quality},
vhead: segment,
audio: -1,
audio: nil,
ahead: -1,
}
return stream.GetVideoSegment(video, quality, segment)
@ -130,7 +130,7 @@ func (t *Transcoder) GetVideoSegment(
func (t *Transcoder) GetAudioSegment(
path string,
audio int32,
audio uint32,
segment int32,
client string,
sha string,
@ -142,7 +142,7 @@ func (t *Transcoder) GetAudioSegment(
t.clientChan <- ClientInfo{
client: client,
path: path,
audio: audio,
audio: &audio,
ahead: segment,
vhead: -1,
}

View File

@ -15,3 +15,13 @@ func printExecTime(message string, args ...any) func() {
log.Printf("%s finished in %s", msg, time.Since(start))
}
}
func Filter[E any](s []E, f func(E) bool) []E {
s2 := make([]E, 0, len(s))
for _, e := range s {
if f(e) {
s2 = append(s2, e)
}
}
return s2
}

View File

@ -7,11 +7,11 @@ import (
type VideoStream struct {
Stream
idx int32
video *Video
quality Quality
}
func (t *Transcoder) NewVideoStream(file *FileStream, idx int32, quality Quality) (*VideoStream, error) {
func (t *Transcoder) NewVideoStream(file *FileStream, idx uint32, quality Quality) (*VideoStream, error) {
log.Printf(
"Creating a new video stream for %s (n %d) in quality %s",
file.Info.Path,
@ -25,8 +25,14 @@ func (t *Transcoder) NewVideoStream(file *FileStream, idx int32, quality Quality
}
ret := new(VideoStream)
ret.idx = idx
ret.quality = quality
for _, video := range file.Info.Videos {
if video.Index == idx {
ret.video = &video
break
}
}
NewStream(file, keyframes, ret, &ret.Stream)
return ret, nil
}
@ -54,7 +60,7 @@ func closestMultiple(n int32, x int32) int32 {
func (vs *VideoStream) getTranscodeArgs(segments string) []string {
args := []string{
"-map", fmt.Sprint("0:V:%d", vs.idx),
"-map", fmt.Sprint("0:V:%d", vs.video.Index),
}
if vs.quality == Original {
@ -65,7 +71,7 @@ func (vs *VideoStream) getTranscodeArgs(segments string) []string {
}
args = append(args, Settings.HwAccel.EncodeFlags...)
width := int32(float64(vs.quality.Height()) / float64(vs.file.Info.Video.Height) * float64(vs.file.Info.Video.Width))
width := int32(float64(vs.quality.Height()) / float64(vs.video.Height) * float64(vs.video.Width))
// force a width that is a multiple of two else some apps behave badly.
width = closestMultiple(width, 2)
args = append(args,