Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Enable id3v2 tags in HLS for all encoders. #3604

Merged
merged 1 commit into from
Dec 22, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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])
Loading