Skip to content

Commit

Permalink
Enable id3v2 tags in HLS for all encoders. (#3604)
Browse files Browse the repository at this point in the history
  • Loading branch information
toots authored Dec 22, 2023
1 parent ee4dc4d commit 118cc1b
Show file tree
Hide file tree
Showing 8 changed files with 152 additions and 43 deletions.
4 changes: 2 additions & 2 deletions doc/content/hls_output.md
Original file line number Diff line number Diff line change
Expand Up @@ -32,8 +32,8 @@ not recommended for listener-facing setup but can be useful to sync up with a ca

HLS outputs supports metadata in two ways:

- Through a `timed_id3` metadata logical stream with the `mpegts` format.
- Through regular ID3 frames, as requested by the [HLS specifications](https://datatracker.ietf.org/doc/html/rfc8216#section-3.4) for `adts`, `mp3`, `ac3` and `eac3` formats.
- Using the `%ffmpeg` encoder, through a `timed_id3` metadata logical stream with the `mpegts` format.
- Through regular ID3 frames, as requested by the [HLS specifications](https://datatracker.ietf.org/doc/html/rfc8216#section-3.4) for `adts`, `mp3`, `ac3` and `eac3` formats with the `%ffmpeg` encoder and also natively using the `%mp3`, `%shine` or `%fdkaac` encoders.
- There is currently no support for in-stream metadata for the `mp4` format.

Metadata parameters are passed through the record methods of the streams' encoders. Here's an example
Expand Down
1 change: 1 addition & 0 deletions src/core/dune
Original file line number Diff line number Diff line change
Expand Up @@ -96,6 +96,7 @@
echo
encoder
encoder_formats
encoder_utils
error
external_decoder
external_encoder
Expand Down
66 changes: 66 additions & 0 deletions src/core/encoder/encoder_utils.ml
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
(*****************************************************************************
Liquidsoap, a programmable audio stream generator.
Copyright 2003-2023 Savonet team
This program is free software; you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation; either version 2 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details, fully stated in the COPYING
file at the root of the liquidsoap distribution.
You should have received a copy of the GNU General Public License
along with this program; if not, write to the Free Software
Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
*****************************************************************************)

(* See: https://datatracker.ietf.org/doc/html/rfc8216#section-3.4 *)
let render_mpeg2_timestamp =
let mpeg2_timestamp_unit = 90000. in
let frame_len =
lazy
(Int64.of_float
(Frame.seconds_of_main (Lazy.force Frame.size) *. mpeg2_timestamp_unit))
in
fun ~frame_position ~sample_position () ->
let buf = Buffer.create 10 in
let frame_position =
Int64.mul (Lazy.force frame_len) (Int64.of_int frame_position)
in
let sample_position =
Int64.of_float
(Frame.seconds_of_main sample_position *. mpeg2_timestamp_unit)
in
let position = Int64.add frame_position sample_position in
let position = Int64.unsigned_rem position 0x1ffffffffL in
Buffer.add_int64_be buf position;
Buffer.contents buf

let mk_hls_id3 ?(id3_version = 3) ~frame_position ~sample_position m =
let timestamp =
Printf.sprintf "com.apple.streaming.transportStreamTimestamp\000%s"
(render_mpeg2_timestamp ~frame_position ~sample_position ())
in
let m = ("PRIV", timestamp) :: m in
Utils.id3v2_of_metadata ~version:id3_version m

let mk_id3_hls ~pos encoder =
let id3_version_ref = ref None in
let init ?id3_enabled ?id3_version () =
id3_version_ref := id3_version;
if id3_enabled = Some false then
Lang_encoder.raise_error ~pos "Format requires ID3 metadata!";
true
in
let insert_id3 ~frame_position ~sample_position m =
Some
(mk_hls_id3 ?id3_version:!id3_version_ref ~frame_position ~sample_position
m)
in
Encoder.{ (dummy_hls encoder) with init; insert_id3 }
2 changes: 1 addition & 1 deletion src/core/encoder/encoders/fdkaac_encoder.ml
Original file line number Diff line number Diff line change
Expand Up @@ -111,7 +111,7 @@ let encoder ~pos aac =
{
Encoder.insert_metadata = (fun _ -> ());
header = (fun () -> Strings.empty);
hls = Encoder.dummy_hls encode;
hls = Encoder_utils.mk_id3_hls ~pos encode;
encode;
stop;
}
Expand Down
33 changes: 3 additions & 30 deletions src/core/encoder/encoders/ffmpeg_encoder_common.ml
Original file line number Diff line number Diff line change
Expand Up @@ -24,28 +24,6 @@

let log = Ffmpeg_utils.log

(* See: https://datatracker.ietf.org/doc/html/rfc8216#section-3.4 *)
let render_mpeg2_timestamp =
let mpeg2_timestamp_unit = 90000. in
let frame_len =
lazy
(Int64.of_float
(Frame.seconds_of_main (Lazy.force Frame.size) *. mpeg2_timestamp_unit))
in
fun ~frame_position ~sample_position () ->
let buf = Buffer.create 10 in
let frame_position =
Int64.mul (Lazy.force frame_len) (Int64.of_int frame_position)
in
let sample_position =
Int64.of_float
(Frame.seconds_of_main sample_position *. mpeg2_timestamp_unit)
in
let position = Int64.add frame_position sample_position in
let position = Int64.unsigned_rem position 0x1ffffffffL in
Buffer.add_int64_be buf position;
Buffer.contents buf

type encoder = {
mk_stream : Frame.t -> unit;
encode : Frame.t -> int -> int -> unit;
Expand Down Expand Up @@ -242,16 +220,11 @@ let encoder ~pos ~mk_streams ffmpeg meta =
| Some "adts" | Some "mp3" | Some "ac3" | Some "eac3" ->
if id3_enabled = Some false then
Lang_encoder.raise_error ~pos "Format requires ID3 metadata!";
let id3_version = Option.value ~default:3 id3_version in
encoder.insert_id3 <-
(fun ~frame_position ~sample_position m ->
let timestamp =
Printf.sprintf
"com.apple.streaming.transportStreamTimestamp\000%s"
(render_mpeg2_timestamp ~frame_position ~sample_position ())
in
let m = ("PRIV", timestamp) :: m in
Some (Utils.id3v2_of_metadata ~version:id3_version m));
Some
(Encoder_utils.mk_hls_id3 ?id3_version ~frame_position
~sample_position m));
true
| Some _ when id3_enabled = Some true ->
Lang_encoder.raise_error ~pos
Expand Down
7 changes: 4 additions & 3 deletions src/core/encoder/encoders/lame_encoder.ml
Original file line number Diff line number Diff line change
Expand Up @@ -75,7 +75,7 @@ let () =
Lame.init_params enc;
enc
in
let mp3_encoder mp3 metadata =
let mp3_encoder ~pos mp3 metadata =
let enc = create_encoder mp3 in
let channels = if mp3.Mp3_format.stereo then 2 else 1 in
(* Lame accepts data of a fixed length.. *)
Expand Down Expand Up @@ -118,12 +118,13 @@ let () =
let stop () = Strings.of_string (Lame.encode_flush enc) in
{
insert_metadata = (fun _ -> ());
hls = Encoder.dummy_hls encode;
hls = Encoder_utils.mk_id3_hls ~pos encode;
encode;
header = (fun () -> Strings.empty);
stop;
}
in
Plug.register Encoder.plug "lame" ~doc:"LAME mp3 encoder." (function
| Encoder.MP3 mp3 -> Some (fun ?hls:_ ~pos:_ _ meta -> mp3_encoder mp3 meta)
| Encoder.MP3 mp3 ->
Some (fun ?hls:_ ~pos _ meta -> mp3_encoder ~pos mp3 meta)
| _ -> None)
6 changes: 3 additions & 3 deletions src/core/encoder/encoders/shine_encoder.ml
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@ open Shine_format
let create_encoder ~samplerate ~bitrate ~channels =
Shine.create { Shine.channels; samplerate; bitrate }

let encoder shine =
let encoder ~pos shine =
let channels = shine.channels in
let samplerate = Lazy.force shine.samplerate in
let enc = create_encoder ~samplerate ~bitrate:shine.bitrate ~channels in
Expand Down Expand Up @@ -67,13 +67,13 @@ let encoder shine =
{
Encoder.insert_metadata = (fun _ -> ());
header = (fun () -> Strings.empty);
hls = Encoder.dummy_hls encode;
hls = Encoder_utils.mk_id3_hls ~pos encode;
encode;
stop;
}

let () =
Plug.register Encoder.plug "shine" ~doc:"SHINE fixed-point mp3 encoder."
(function
| Encoder.Shine m -> Some (fun ?hls:_ ~pos:_ _ _ -> encoder m)
| Encoder.Shine m -> Some (fun ?hls:_ ~pos _ _ -> encoder ~pos m)
| _ -> None)
76 changes: 72 additions & 4 deletions tests/streams/hls_id3v2.liq
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,9 @@ output.file.hls(
%ffmpeg(format = "mpegts", %audio(codec = "aac")).{id3_version=4}
),
("ts", %ffmpeg(format = "mpegts", %audio(codec = "aac")).{id3=false}),
("shine", %shine),
("lame", %mp3),
("fdkaac", %fdkaac),
(
"mp4",
%ffmpeg(
Expand All @@ -39,17 +42,30 @@ output.file.hls(
s
)

to_check = ref({aac=null(), ts_with_meta=null(), ts=null(), mp4=null()})
to_check =
ref(
{
aac=null(),
shine=null(),
lame=null(),
fdkaac=null(),
ts_with_meta=null(),
ts=null(),
mp4=null()
}
)

def check_done() =
let {aac, ts_with_meta, ts, mp4} = to_check()
let {aac, shine, lame, fdkaac, ts_with_meta, ts, mp4} = to_check()

if
null.defined(ts)
then
test.fail(
"ts shouldn't have metadata!"
)
end

if
null.defined(mp4)
then
Expand All @@ -59,10 +75,12 @@ def check_done() =
end

if
null.defined(aac) and null.defined(ts_with_meta)
null.defined(aac) and null.defined(fdkaac) and null.defined(ts_with_meta)
then
aac = null.get(aac)
fdkaac = null.get(fdkaac)
ts_with_meta = null.get(ts_with_meta)

if

aac["title"] ==
Expand All @@ -72,6 +90,13 @@ def check_done() =
aac["album"] == "foolol"
and

fdkaac["title"] ==
"test title"

and
fdkaac["album"] == "foolol"
and

ts_with_meta["title"] ==
"test title"

Expand All @@ -95,6 +120,49 @@ aac =
)

output.dummy(fallible=true, aac)

#< FFMPEG seems to be unable to parse id3v2 metadata
inside mp3 streams..
shine = input.hls("#{tmp_dir}/shine.m3u8")
shine =
source.on_metadata(
shine,
fun (m) ->
begin
if m["title"] != "" then to_check := to_check().{shine=m} end
check_done()
end
)

output.dummy(fallible=true, shine)

lame = input.hls("#{tmp_dir}/lame.m3u8")
lame =
source.on_metadata(
lame,
fun (m) ->
begin
if m["title"] != "" then to_check := to_check().{lame=m} end
check_done()
end
)

output.dummy(fallible=true, lame)
>#

fdkaac = input.hls("#{tmp_dir}/fdkaac.m3u8")
fdkaac =
source.on_metadata(
fdkaac,
fun (m) ->
begin
if m["title"] != "" then to_check := to_check().{fdkaac=m} end
check_done()
end
)

output.dummy(fallible=true, fdkaac)

ts_with_meta = input.hls("#{tmp_dir}/ts_with_meta.m3u8")
ts_with_meta =
source.on_metadata(
Expand Down Expand Up @@ -131,4 +199,4 @@ mp4 =
)

output.dummy(fallible=true, mp4)
clock.assign_new(sync="none", [s, aac, ts_with_meta, ts, mp4])
clock.assign_new(sync="none", [s, aac, fdkaac, ts_with_meta, ts, mp4])

0 comments on commit 118cc1b

Please sign in to comment.