diff --git a/CHANGES.md b/CHANGES.md index 0a67b1ccd6..b4f77005dc 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -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) diff --git a/src/core/builtins/builtins_request.ml b/src/core/builtins/builtins_request.ml index 3d20fee528..1cd2b8a8a6 100644 --- a/src/core/builtins/builtins_request.ml +++ b/src/core/builtins/builtins_request.ml @@ -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 diff --git a/src/core/decoder/ffmpeg_decoder.ml b/src/core/decoder/ffmpeg_decoder.ml index 054ac0179f..06ed6e552d 100644 --- a/src/core/decoder/ffmpeg_decoder.ml +++ b/src/core/decoder/ffmpeg_decoder.ml @@ -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); diff --git a/src/core/request.ml b/src/core/request.ml index bc7e404d27..11407289be 100644 --- a/src/core/request.ml +++ b/src/core/request.ml @@ -160,7 +160,7 @@ 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 }) -> @@ -168,6 +168,9 @@ let compute_duration ~metadata file = 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 @@ -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, @@ -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 diff --git a/src/core/request.mli b/src/core/request.mli index a17b637942..f5052c6558 100644 --- a/src/core/request.mli +++ b/src/core/request.mli @@ -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 diff --git a/src/libs/autocue.liq b/src/libs/autocue.liq index 0737598c58..1f9c8fb83e 100644 --- a/src/libs/autocue.liq +++ b/src/libs/autocue.liq @@ -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