diff --git a/transcoder/main.go b/transcoder/main.go index 3c1994cd..e36975b3 100644 --- a/transcoder/main.go +++ b/transcoder/main.go @@ -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 } diff --git a/transcoder/src/audiostream.go b/transcoder/src/audiostream.go index 8a1dee20..24f561a4 100644 --- a/transcoder/src/audiostream.go +++ b/transcoder/src/audiostream.go @@ -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) diff --git a/transcoder/src/filestream.go b/transcoder/src/filestream.go index 41ddc193..5e60e6bd 100644 --- a/transcoder/src/filestream.go +++ b/transcoder/src/filestream.go @@ -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 diff --git a/transcoder/src/info.go b/transcoder/src/info.go index 39beab01..2936c93b 100644 --- a/transcoder/src/info.go +++ b/transcoder/src/info.go @@ -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 } diff --git a/transcoder/src/keyframes.go b/transcoder/src/keyframes.go index 6a442c5c..9c7c38c1 100644 --- a/transcoder/src/keyframes.go +++ b/transcoder/src/keyframes.go @@ -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) diff --git a/transcoder/src/metadata.go b/transcoder/src/metadata.go index 6ea76683..284bb3ba 100644 --- a/transcoder/src/metadata.go +++ b/transcoder/src/metadata.go @@ -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) } diff --git a/transcoder/src/quality.go b/transcoder/src/quality.go index 68c9aaf2..79ae5cd6 100644 --- a/transcoder/src/quality.go +++ b/transcoder/src/quality.go @@ -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 } } diff --git a/transcoder/src/tracker.go b/transcoder/src/tracker.go index 2d59ff40..57d8dc48 100644 --- a/transcoder/src/tracker.go +++ b/transcoder/src/tracker.go @@ -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) } diff --git a/transcoder/src/transcoder.go b/transcoder/src/transcoder.go index de011583..d20eef50 100644 --- a/transcoder/src/transcoder.go +++ b/transcoder/src/transcoder.go @@ -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, } diff --git a/transcoder/src/utils.go b/transcoder/src/utils.go index 5277def3..577c4f6b 100644 --- a/transcoder/src/utils.go +++ b/transcoder/src/utils.go @@ -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 +} diff --git a/transcoder/src/videostream.go b/transcoder/src/videostream.go index 02264467..85df7d38 100644 --- a/transcoder/src/videostream.go +++ b/transcoder/src/videostream.go @@ -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,