From d868ad9e613d2f20676d2d9f6f2c0b245aa58da2 Mon Sep 17 00:00:00 2001 From: Zoe Roux Date: Thu, 2 Apr 2026 15:18:22 +0200 Subject: [PATCH 1/2] Add `channels` to audio info --- transcoder/migrations/000003_add_audio_channels.down.sql | 5 +++++ transcoder/migrations/000003_add_audio_channels.up.sql | 5 +++++ transcoder/src/codec.go | 4 ++-- transcoder/src/info.go | 5 ++++- transcoder/src/metadata.go | 5 +++-- 5 files changed, 19 insertions(+), 5 deletions(-) create mode 100644 transcoder/migrations/000003_add_audio_channels.down.sql create mode 100644 transcoder/migrations/000003_add_audio_channels.up.sql diff --git a/transcoder/migrations/000003_add_audio_channels.down.sql b/transcoder/migrations/000003_add_audio_channels.down.sql new file mode 100644 index 00000000..2f5955f7 --- /dev/null +++ b/transcoder/migrations/000003_add_audio_channels.down.sql @@ -0,0 +1,5 @@ +begin; + +alter table gocoder.audios drop column channels; + +commit; diff --git a/transcoder/migrations/000003_add_audio_channels.up.sql b/transcoder/migrations/000003_add_audio_channels.up.sql new file mode 100644 index 00000000..b32f6c83 --- /dev/null +++ b/transcoder/migrations/000003_add_audio_channels.up.sql @@ -0,0 +1,5 @@ +begin; + +alter table gocoder.audios add column channels int not null default 2; + +commit; diff --git a/transcoder/src/codec.go b/transcoder/src/codec.go index acfe8839..9c98c110 100644 --- a/transcoder/src/codec.go +++ b/transcoder/src/codec.go @@ -100,11 +100,11 @@ func GetMimeCodec(stream *ffprobe.Stream) *string { return &ret case "ac3": - ret := "mp4a.a5" + ret := "ac-3" return &ret case "eac3": - ret := "mp4a.a6" + ret := "ec-3" return &ret case "flac": diff --git a/transcoder/src/info.go b/transcoder/src/info.go index 3860aa63..e6761b97 100644 --- a/transcoder/src/info.go +++ b/transcoder/src/info.go @@ -17,7 +17,7 @@ import ( "gopkg.in/vansante/go-ffprobe.v2" ) -const InfoVersion = 3 +const InfoVersion = 4 type Versions struct { Info int32 `json:"info" db:"ver_info"` @@ -98,6 +98,8 @@ type Audio struct { Codec string `json:"codec" db:"codec"` /// The codec of this stream (defined as the RFC 6381). MimeCodec *string `json:"mimeCodec" db:"mime_codec"` + /// The number of channels that stream has. + Channels int `json:"channels" db:"channels"` /// The average bitrate of the audio in bytes/s Bitrate uint32 `json:"bitrate" db:"bitrate"` /// Is this stream the default one of it's type? @@ -280,6 +282,7 @@ func RetriveMediaInfo(path string, sha string) (*MediaInfo, error) { Language: NullIfUnd(lang.String()), Codec: stream.CodecName, MimeCodec: GetMimeCodec(stream), + Channels: stream.Channels, Bitrate: ParseUint(cmp.Or(stream.BitRate, mi.Format.BitRate)), IsDefault: stream.Disposition.Default != 0, } diff --git a/transcoder/src/metadata.go b/transcoder/src/metadata.go index df53eee3..ced4d9b0 100644 --- a/transcoder/src/metadata.go +++ b/transcoder/src/metadata.go @@ -319,7 +319,7 @@ func (s *MetadataService) storeFreshMetadata(ctx context.Context, path string, s ctx, ` insert into gocoder.audios(sha, idx, title, language, codec, mime_codec, is_default, bitrate) - values ($1, $2, $3, $4, $5, $6, $7, $8) + values ($1, $2, $3, $4, $5, $6, $7, $8, $9) on conflict (sha, idx) do update set sha = excluded.sha, idx = excluded.idx, @@ -327,10 +327,11 @@ func (s *MetadataService) storeFreshMetadata(ctx context.Context, path string, s language = excluded.language, codec = excluded.codec, mime_codec = excluded.mime_codec, + channels = excluded.channels, is_default = excluded.is_default, bitrate = excluded.bitrate `, - ret.Sha, a.Index, a.Title, a.Language, a.Codec, a.MimeCodec, a.IsDefault, a.Bitrate, + ret.Sha, a.Index, a.Title, a.Language, a.Codec, a.MimeCodec, a.Channels, a.IsDefault, a.Bitrate, ) } for _, s := range ret.Subtitles { From 480101e3780b86abde884dfb66eeee5efabbd5c4 Mon Sep 17 00:00:00 2001 From: Zoe Roux Date: Thu, 2 Apr 2026 15:18:22 +0200 Subject: [PATCH 2/2] Cleanup master.m3u8 --- front/public/translations/en.json | 1 + front/src/models/video-info.ts | 1 + front/src/ui/info/index.tsx | 1 + .../000003_add_audio_channels.up.sql | 1 + transcoder/src/filestream.go | 124 ++++++++++-------- transcoder/src/metadata.go | 2 +- 6 files changed, 76 insertions(+), 54 deletions(-) diff --git a/front/public/translations/en.json b/front/public/translations/en.json index 78e33aa9..6fe0a950 100644 --- a/front/public/translations/en.json +++ b/front/public/translations/en.json @@ -311,6 +311,7 @@ "video": "Video", "audio": "Audio", "subtitles": "Subtitles", + "channels": "{{channels}} Channels", "forced": "Forced", "hearing-impaired": "CC", "default": "Default", diff --git a/front/src/models/video-info.ts b/front/src/models/video-info.ts index 506599bf..80e0466d 100644 --- a/front/src/models/video-info.ts +++ b/front/src/models/video-info.ts @@ -35,6 +35,7 @@ export const AudioTrack = z.object({ language: z.string().nullable(), codec: z.string(), mimeCodec: z.string().nullable(), + channels: z.int(), bitrate: z.number(), isDefault: z.boolean(), }); diff --git a/front/src/ui/info/index.tsx b/front/src/ui/info/index.tsx index 8bd7377a..a78c6a2f 100644 --- a/front/src/ui/info/index.tsx +++ b/front/src/ui/info/index.tsx @@ -94,6 +94,7 @@ export const Info = () => { ? t("mediainfo.default") : undefined, x.codec, + t("mediainfo.channels", { channels: x.channels }), ] .filter((x) => x) .join(" - ")} diff --git a/transcoder/migrations/000003_add_audio_channels.up.sql b/transcoder/migrations/000003_add_audio_channels.up.sql index b32f6c83..f0d5140f 100644 --- a/transcoder/migrations/000003_add_audio_channels.up.sql +++ b/transcoder/migrations/000003_add_audio_channels.up.sql @@ -1,5 +1,6 @@ begin; +delete from gocoder.audios; alter table gocoder.audios add column channels int not null default 2; commit; diff --git a/transcoder/src/filestream.go b/transcoder/src/filestream.go index 9f9637fb..2efd7830 100644 --- a/transcoder/src/filestream.go +++ b/transcoder/src/filestream.go @@ -77,52 +77,12 @@ func (fs *FileStream) Destroy() { func (fs *FileStream) GetMaster(client string) string { master := "#EXTM3U\n" - for _, audio := range fs.Info.Audios { - for _, quality := range AudioQualities { - master += "#EXT-X-MEDIA:TYPE=AUDIO," - master += fmt.Sprintf("GROUP-ID=\"audio-%s\",", quality) - if audio.Language != nil { - master += fmt.Sprintf("LANGUAGE=\"%s\",", *audio.Language) - } - if audio.Title != nil { - master += fmt.Sprintf("NAME=\"%s\",", *audio.Title) - } else if audio.Language != nil { - master += fmt.Sprintf("NAME=\"%s\",", *audio.Language) - } else { - master += fmt.Sprintf("NAME=\"Audio %d\",", audio.Index) - } - if audio.IsDefault { - master += "DEFAULT=YES," - } - master += "CHANNELS=\"2\"," - master += fmt.Sprintf("URI=\"audio/%d/%s/index.m3u8?clientId=%s\"\n", audio.Index, quality, client) - } - master += "#EXT-X-MEDIA:TYPE=AUDIO," - master += fmt.Sprintf("GROUP-ID=\"audio-%s\",", AOriginal) - if audio.Language != nil { - master += fmt.Sprintf("LANGUAGE=\"%s\",", *audio.Language) - } - if audio.Title != nil { - master += fmt.Sprintf("NAME=\"%s\",", *audio.Title) - } else if audio.Language != nil { - master += fmt.Sprintf("NAME=\"%s\",", *audio.Language) - } else { - master += fmt.Sprintf("NAME=\"Audio %d\",", audio.Index) - } - if audio.IsDefault { - master += "DEFAULT=YES," - } - master += "CHANNELS=\"2\"," // TODO - master += fmt.Sprintf("URI=\"audio/%d/%s/index.m3u8?clientId=%s\"\n", audio.Index, AOriginal, client) - } - // codec is the prefix + the level, the level is not part of the codec we want to compare for the same_codec check bellow transcode_prefix := "avc1.6400" transcode_codec := transcode_prefix + "28" transcode_audio_codec := "mp4a.40.2" var def_video *Video - var def_audio *Audio for _, video := range fs.Info.Videos { if video.IsDefault { def_video = &video @@ -132,10 +92,53 @@ func (fs *FileStream) GetMaster(client string) string { if def_video == nil && len(fs.Info.Videos) > 0 { def_video = &fs.Info.Videos[0] } - if len(fs.Info.Audios) > 0 { + + var def_audio *Audio + for _, audio := range fs.Info.Audios { + if audio.IsDefault { + def_audio = &audio + break + } + } + if def_audio == nil && len(fs.Info.Audios) > 0 { def_audio = &fs.Info.Audios[0] } + if def_audio != nil { + aqualities := utils.Filter(AudioQualities, func(quality AudioQuality) bool { + return quality.Bitrate() < def_audio.Bitrate + }) + aqualities = append(aqualities, AOriginal) + + for _, audio := range fs.Info.Audios { + for _, quality := range slices.Backward(aqualities) { + master += "#EXT-X-MEDIA:TYPE=AUDIO," + master += fmt.Sprintf("GROUP-ID=\"a-%s\",", quality) + if audio.Language != nil { + master += fmt.Sprintf("LANGUAGE=\"%s\",", *audio.Language) + } + if audio.Title != nil { + master += fmt.Sprintf("NAME=\"%s\",", *audio.Title) + } else if audio.Language != nil { + master += fmt.Sprintf("NAME=\"%s\",", *audio.Language) + } else { + master += fmt.Sprintf("NAME=\"Audio %d\",", audio.Index) + } + if audio == *def_audio { + master += "DEFAULT=YES," + } + if quality == AOriginal { + master += fmt.Sprintf("CHANNELS=\"%d\",", audio.Channels) + } else { + master += "CHANNELS=\"2\"," + } + master += fmt.Sprintf("URI=\"audio/%d/%s/index.m3u8?clientId=%s\"\n", audio.Index, quality, client) + } + master += "\n" + } + master += "\n" + } + if def_video != nil { qualities := utils.Filter(VideoQualities, func(quality VideoQuality) bool { return quality.Height() < def_video.Height @@ -148,8 +151,8 @@ func (fs *FileStream) GetMaster(client string) string { } qualities = append(qualities, Original) - for _, quality := range qualities { - for _, video := range fs.Info.Videos { + for _, video := range fs.Info.Videos { + for _, quality := range slices.Backward(qualities) { master += "#EXT-X-MEDIA:TYPE=VIDEO," master += fmt.Sprintf("GROUP-ID=\"%s\",", quality) if video.Language != nil { @@ -168,6 +171,7 @@ func (fs *FileStream) GetMaster(client string) string { master += fmt.Sprintf("URI=\"%d/%s/index.m3u8?clientId=%s\"\n", video.Index, quality, client) } } + master += "\n" } master += "\n" @@ -178,23 +182,35 @@ func (fs *FileStream) GetMaster(client string) string { if def_audio != nil && (def_audio.MimeCodec == nil || *def_audio.MimeCodec != transcode_audio_codec) { audios = append(audios, matchAudioQuality(def_video.Quality())) } - for _, audio_quality := range audios { + for _, aquality := range audios { // original & noresize streams 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) - var audio_codec = transcode_audio_codec - if def_audio != nil && audio_quality == AOriginal { - audio_codec = *def_audio.MimeCodec + + codecs := make([]string, 0) + if quality == Original { + if def_video.MimeCodec != nil { + codecs = append(codecs, *def_video.MimeCodec) + } + } else { + codecs = append(codecs, transcode_codec) } - if quality != Original { - master += fmt.Sprintf("CODECS=\"%s\",", strings.Join([]string{transcode_codec, audio_codec}, ",")) - } else if def_video.MimeCodec != nil { - master += fmt.Sprintf("CODECS=\"%s\",", strings.Join([]string{*def_video.MimeCodec, audio_codec}, ",")) + if aquality == AOriginal { + if def_audio != nil && def_audio.MimeCodec != nil { + codecs = append(codecs, *def_audio.MimeCodec) + } + } else { + codecs = append(codecs, transcode_audio_codec) + } + if len(codecs) > 0 { + master += fmt.Sprintf("CODECS=\"%s\",", strings.Join(codecs, ",")) + } + if def_audio != nil { + master += fmt.Sprintf("AUDIO=\"a-%s\",", string(aquality)) } - master += fmt.Sprintf("AUDIO=\"audio-%s\",", string(audio_quality)) master += "CLOSED-CAPTIONS=NONE\n" master += fmt.Sprintf("%d/%s/index.m3u8?clientId=%s\n", def_video.Index, quality, client) } @@ -206,7 +222,9 @@ func (fs *FileStream) GetMaster(client string) string { 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{transcode_codec, transcode_audio_codec}, ",")) - master += fmt.Sprintf("AUDIO=\"audio-%s\",", string(matchAudioQuality(quality))) + if def_audio != nil { + master += fmt.Sprintf("AUDIO=\"a-%s\",", string(matchAudioQuality(quality))) + } master += "CLOSED-CAPTIONS=NONE\n" master += fmt.Sprintf("%d/%s/index.m3u8?clientId=%s\n", def_video.Index, quality, client) } diff --git a/transcoder/src/metadata.go b/transcoder/src/metadata.go index ced4d9b0..4a79c912 100644 --- a/transcoder/src/metadata.go +++ b/transcoder/src/metadata.go @@ -318,7 +318,7 @@ func (s *MetadataService) storeFreshMetadata(ctx context.Context, path string, s tx.Exec( ctx, ` - insert into gocoder.audios(sha, idx, title, language, codec, mime_codec, is_default, bitrate) + insert into gocoder.audios(sha, idx, title, language, codec, mime_codec, channels, is_default, bitrate) values ($1, $2, $3, $4, $5, $6, $7, $8, $9) on conflict (sha, idx) do update set sha = excluded.sha,