Skip to content

Commit

Permalink
Add request.duration.<decoder>, use it to get a precise duration in
Browse files Browse the repository at this point in the history
autocue.
  • Loading branch information
toots committed Oct 9, 2024
1 parent ea1b3ee commit be448ef
Show file tree
Hide file tree
Showing 6 changed files with 135 additions and 77 deletions.
2 changes: 1 addition & 1 deletion CHANGES.md
Original file line number Diff line number Diff line change
Expand Up @@ -150,7 +150,7 @@ Fixed:
- Make sure reconnection errors are router through the regulat `on_error` callback in `output.icecast` (#3635)
- Fixed discontinuity count after a restart in HLS outputs.
- Fixed file header logic when reopening in `output.file` (#3675)
- Fixed memory leaks when using dynamically created sources (`input.harbor`, `input.ffmepg`, SRT sources and `request.dynamic`)
- Fixed memory leaks when using dynamically created sources (`input.harbor`, `input.ffmpeg`, SRT sources and `request.dynamic`)
- Fixed invalid array fill in `add` (#3678)
- Fixed deadlock when connecting to a non-SSL icecast using the TLS transport (#3681)
- Fixed crash when closing external process (#3685)
Expand Down
113 changes: 71 additions & 42 deletions src/core/builtins/builtins_request.ml
Original file line number Diff line number Diff line change
Expand Up @@ -185,48 +185,77 @@ let _ =
Lang.unit)

let _ =
Lang.add_builtin ~base:request "duration" ~category:`Liquidsoap
[
( "resolve_metadata",
Lang.bool_t,
Some (Lang.bool true),
Some "Set to `false` to prevent metadata resolution on this request." );
( "metadata",
Lang.metadata_t,
Some (Lang.list []),
Some "Optional metadata used to decode the file, e.g. `ffmpeg_options`."
);
( "timeout",
Lang.float_t,
Some (Lang.float 30.),
Some "Limit in seconds to the duration of the resolving." );
("", Lang.string_t, None, None);
]
(Lang.nullable_t Lang.float_t)
~descr:
"Compute the duration in seconds of audio data contained in a request. \
The computation may be expensive. Returns `null` if computation failed, \
typically if the file was not recognized as valid audio."
(fun p ->
let f = Lang.to_string (List.assoc "" p) in
let resolve_metadata = Lang.to_bool (List.assoc "resolve_metadata" p) in
let metadata = Lang.to_metadata (List.assoc "metadata" p) in
let timeout = Lang.to_float (List.assoc "timeout" p) in
let r =
Request.create ~resolve_metadata ~metadata ~cue_in_metadata:None
~cue_out_metadata:None f
in
if Request.resolve r timeout = `Resolved then (
match
Request.duration ~metadata:(Request.metadata r)
(Option.get (Request.get_filename r))
with
| Some f -> Lang.float f
| None -> Lang.null
| exception exn ->
let bt = Printexc.get_raw_backtrace () in
Lang.raise_as_runtime ~bt ~kind:"failure" exn)
else Lang.null)
let add_duration_resolver ~base ~name ~resolver () =
Lang.add_builtin ~base name ~category:`Liquidsoap
((if resolver = None then
[
( "resolvers",
Lang.nullable_t (Lang.list_t Lang.string_t),
Some Lang.null,
Some
"Set to a list of resolvers to only resolve duration using a \
specific decoder." );
]
else [])
@ [
( "resolve_metadata",
Lang.bool_t,
Some (Lang.bool true),
Some
"Set to `false` to prevent metadata resolution on this request."
);
( "metadata",
Lang.metadata_t,
Some (Lang.list []),
Some
"Optional metadata used to decode the file, e.g. \
`ffmpeg_options`." );
( "timeout",
Lang.float_t,
Some (Lang.float 30.),
Some "Limit in seconds to the duration of the resolving." );
("", Lang.string_t, None, None);
])
(Lang.nullable_t Lang.float_t)
~descr:
"Compute the duration in seconds of audio data contained in a request. \
The computation may be expensive. Returns `null` if computation \
failed, typically if the file was not recognized as valid audio."
(fun p ->
let f = Lang.to_string (List.assoc "" p) in
let resolve_metadata = Lang.to_bool (List.assoc "resolve_metadata" p) in
let resolvers =
match resolver with
| None ->
Option.map (List.map Lang.to_string)
(Lang.to_valued_option Lang.to_list (List.assoc "resolvers" p))
| Some r -> Some [r]
in
let metadata = Lang.to_metadata (List.assoc "metadata" p) in
let timeout = Lang.to_float (List.assoc "timeout" p) in
let r =
Request.create ~resolve_metadata ~metadata ~cue_in_metadata:None
~cue_out_metadata:None f
in
if Request.resolve r timeout = `Resolved then (
match
Request.duration ?resolvers ~metadata:(Request.metadata r)
(Option.get (Request.get_filename r))
with
| Some f -> Lang.float f
| None -> Lang.null
| exception exn ->
let bt = Printexc.get_raw_backtrace () in
Lang.raise_as_runtime ~bt ~kind:"failure" exn)
else Lang.null)
in
let base =
add_duration_resolver ~base:request ~name:"duration" ~resolver:None ()
in
List.iter
(fun name ->
ignore (add_duration_resolver ~base ~name ~resolver:(Some name) ()))
Request.conf_dresolvers#get

let _ =
Lang.add_builtin ~base:request "id" ~category:`Liquidsoap
Expand Down
2 changes: 1 addition & 1 deletion src/core/decoder/ffmpeg_decoder.ml
Original file line number Diff line number Diff line change
Expand Up @@ -558,7 +558,7 @@ let dresolver ~metadata file =
Option.map (fun d -> Int64.to_float d /. 1000.) duration)

let () =
Plug.register Request.dresolvers "ffmepg" ~doc:""
Plug.register Request.dresolvers "ffmpeg" ~doc:""
{
dpriority = (fun () -> priority#get);
file_extensions = (fun () -> file_extensions#get);
Expand Down
9 changes: 6 additions & 3 deletions src/core/request.ml
Original file line number Diff line number Diff line change
Expand Up @@ -160,14 +160,17 @@ let get_dresolvers ~file () =
(fun (_, a) (_, b) -> compare (b.dpriority ()) (a.dpriority ()))
resolvers

let compute_duration ~metadata file =
let compute_duration ?resolvers ~metadata file =
try
List.iter
(fun (name, { dpriority; dresolver }) ->
try
log#info "Trying duration resolver %s (priority: %d) for file %s.."
name (dpriority ())
(Lang_string.quote_string file);
(match resolvers with
| Some l when not (List.mem name l) -> raise Not_found
| _ -> ());
let ans = dresolver ~metadata file in
raise (Duration ans)
with
Expand All @@ -177,7 +180,7 @@ let compute_duration ~metadata file =
raise Not_found
with Duration d -> d

let duration ~metadata file =
let duration ?resolvers ~metadata file =
try
match
( Frame.Metadata.find_opt "duration" metadata,
Expand All @@ -189,7 +192,7 @@ let duration ~metadata file =
| _, None, Some cue_out -> Some (float_of_string cue_out)
| Some v, _, _ -> Some (float_of_string v)
| None, cue_in, None ->
let duration = compute_duration ~metadata file in
let duration = compute_duration ?resolvers ~metadata file in
let duration =
match cue_in with
| Some cue_in -> duration -. float_of_string cue_in
Expand Down
11 changes: 8 additions & 3 deletions src/core/request.mli
Original file line number Diff line number Diff line change
Expand Up @@ -147,10 +147,15 @@ val log : t -> string
These operations are only meaningful for media requests, and might raise
exceptions otherwise. *)

(** [duration ~metadata filename] computes the duration of audio data contained in
[filename]. The computation may be expensive.
(** Duration resolvers. *)
val conf_dresolvers : string list Dtools.Conf.t

(** [duration ?resolvers ~metadata filename] computes the duration of audio data contained in
[filename]. The computation may be expensive. Set [resolvers] to a list of specific decoders
to use for getting duration.
@raise Not_found if no duration computation method is found. *)
val duration : metadata:Frame.metadata -> string -> float option
val duration :
?resolvers:string list -> metadata:Frame.metadata -> string -> float option

(** [true] is a decoder exists for the given content-type. *)
val has_decoder : ctype:Frame.content_type -> t -> bool
Expand Down
75 changes: 48 additions & 27 deletions src/libs/autocue.liq
Original file line number Diff line number Diff line change
Expand Up @@ -715,36 +715,57 @@ def autocue.internal.implementation(
end
end

# Get very last frame for precise track duration
frame = list.last(frames)
frame_duration =
float_of_string(
list.assoc(default="0.", "lavfi.liq.duration_time", frame)
)
# Finalize cue/cross/fade values now...
if
cue_out() == 0.
then
file_duration =
begin
# Duration from the filter frames can be tricky so we first try a proper duration:
request_duration =
%ifdef request.duration.ffmpeg
request.duration.ffmpeg(resolve_metadata=false, filename)
%else
null()
%endif

frame_duration =
if
frame_duration != 100.
then
log.important(
label="autocue",
"Warning: reported frame duration should be 100ms. Either the FFmpeg \
ebur128 filter has changed its internals or the version/build of \
FFmpeg you are using is buggy. We recommend using a fairly recent \
distribution with FFmpeg version 7 or above. Backported packages \
can be tricky."
)
100.
else
frame_duration
end
if
null.defined(request_duration)
then
null.get(request_duration)
else
# Get very last frame for precise track duration
frame_duration =
float_of_string(
list.assoc(default="0.", "lavfi.liq.duration_time", frame)
)

duration =
float_of_string(list.assoc(default="0.", "lavfi.liq.pts_time", frame)) +
frame_duration
frame_duration =
if
frame_duration != 0.1
then
log.important(
label="autocue",
"Warning: reported frame duration should be 100ms. Either \
the FFmpeg ebur128 filter has changed its internals or the \
version/build of FFmpeg you are using is buggy. We \
recommend using a fairly recent distribution with FFmpeg \
version 7 or above. Backported packages can be tricky."
)
0.1
else
frame_duration
end

float_of_string(
list.assoc(default="0.", "lavfi.liq.pts_time", frame)
) +
frame_duration
end
end

# Finalize cue/cross/fade values now...
if cue_out() == 0. then cue_out := duration end
cue_out := file_duration
end

# Calc cross/overlap duration
if
Expand Down

0 comments on commit be448ef

Please sign in to comment.