Skip to content
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
2 changes: 1 addition & 1 deletion .github/ISSUE_TEMPLATE/bug_report.yml
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ body:
```
cargo tree -p rsmediainfo
```
placeholder: "0.1.0"
placeholder: "0.2.0"
validations:
required: true

Expand Down
17 changes: 16 additions & 1 deletion .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -94,6 +94,21 @@ jobs:
env:
RUSTDOCFLAGS: -Dwarnings

semver:
name: Semver Check
runs-on: ubuntu-latest
env:
RS_MEDIAINFO_SKIP_DOWNLOAD: "1"
steps:
- uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4.3.1
- uses: dtolnay/rust-toolchain@efa25f7f19611383d5b0ccf2d1c8914531636bf9 # stable
with:
toolchain: stable
- uses: Swatinem/rust-cache@e18b497796c12c097a38f9edb9d0641fb99eee32 # v2.9.1
- name: Install cargo-semver-checks
run: cargo install cargo-semver-checks --locked
- run: cargo semver-checks -p rsmediainfo

audit:
name: Security Audit
runs-on: ubuntu-latest
Expand Down Expand Up @@ -134,7 +149,7 @@ jobs:
release:
name: GitHub Release
runs-on: ubuntu-latest
needs: [fmt, clippy, test, msrv, docs, audit, check-version]
needs: [fmt, clippy, test, msrv, docs, semver, audit, check-version]
if: github.event_name == 'push' && needs.check-version.outputs.new_release == 'true'
permissions:
contents: write
Expand Down
26 changes: 26 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,3 +1,29 @@
# 0.2.0 (April 11, 2026)

### Added
- `MediaInfoContext` — reusable parse context that loads the MediaInfo shared
library once and reuses it across every parse call. Wrap it in `Arc` and
share it across a worker pool for batch workloads; parse calls are still
serialized internally to keep the library's global option store
deterministic.
- `MediaInfo::reset_default_context()` — rare-path escape hatch that drops
the cached process-wide default context so the next free-function parse
rebuilds it from scratch. Useful when recovering from a load failure or
picking up a newly installed library mid-process.
- `MediaInfoError::LibraryMismatch` — returned when a context parse call is
given a `library_file` or `library_search_dir` override that would require
a different shared library than the one the context already loaded.

### Changed
- Free `MediaInfo::parse*` functions now route through a lazily initialized
process-wide default context when no `library_file`/`library_search_dir`
override is set. Callers that pin an explicit library path still go
through the per-call load path, preserving v0.1.0 behavior verbatim.
- The library stays mapped into the process after the first successful
parse (instead of being dlclosed at the end of every call). This matches
the behavior other language wrappers rely on and is the whole reason the
reuse path is faster.

# 0.1.0 (April 11, 2026)

- Initial crate release
7 changes: 6 additions & 1 deletion Cargo.toml
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
[package]
name = "rsmediainfo"
version = "0.1.0"
version = "0.2.0"
edition = "2024"
rust-version = "1.88"
description = "Rust wrapper for MediaInfo library"
Expand Down Expand Up @@ -30,6 +30,11 @@ libc = "0.2"
tempfile = "3.8"
tiny_http = "0.12"
reqwest = { version = "0.12", features = ["blocking", "rustls-tls"] }
criterion = "0.5"

[[bench]]
name = "parse_overhead"
harness = false

[features]
default = ["bundled"]
Expand Down
7 changes: 4 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@
- **Multiple output formats** — text, JSON, XML, or `%`-delimited templates straight from the library
- **Pre-parsed XML** — build a `MediaInfo` from an existing XML payload with no shared library required
- **Bundled library** — optional download of the MediaInfo shared library at build time
- **Reusable context** — `MediaInfoContext` loads the shared library once and reuses it across many parse calls for batch workloads
- **Configurable parsing** — parse speed, completion level, cover art, custom library options, encoding error policy
- **Typed errors** — every failure mode of the underlying library surfaced through `MediaInfoError`
- **Thread-safe** — internal serialization keeps the library's global option store deterministic across threads
Expand All @@ -30,10 +31,10 @@

```toml
[dependencies]
rsmediainfo = "0.1.0"
rsmediainfo = "0.2.0"

# Use a system-installed MediaInfo library instead of the bundled one:
# rsmediainfo = { version = "0.1.0", default-features = false }
# rsmediainfo = { version = "0.2.0", default-features = false }
```

## Feature Flags
Expand All @@ -42,7 +43,7 @@ rsmediainfo = "0.1.0"
|:---|:---|
| `bundled` | Download the MediaInfo shared library for the target platform during the build (enabled by default) |

> See the [`examples/`](examples/) directory for parsing from paths, readers, URLs, raw output, and pre-generated XML.
> See the [`examples/`](examples/) directory for parsing from paths, readers, URLs, raw output, pre-generated XML, and batch parsing through a reusable `MediaInfoContext`.

## License

Expand Down
81 changes: 81 additions & 0 deletions benches/parse_overhead.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,81 @@
//! Micro-benchmark comparing the reusable [`MediaInfoContext`] path
//! with the free function path.
//!
//! After the default-context routing landed, the free function path
//! also benefits from library caching — so the two numbers will be
//! very close once the process has warmed up. To measure the original
//! v0.1.0 "fresh dlopen per call" cost, a third benchmark forces a
//! per-call library load by pinning `library_file` in the
//! [`ParseOptions`]. That override bypasses the default-context fast
//! path through the free function path, which reproduces the old
//! behavior without having to check out an older commit.

use criterion::{Criterion, criterion_group, criterion_main};
use rsmediainfo::{MediaInfo, MediaInfoContext, ParseOptions};
use std::path::PathBuf;

fn sample_path() -> PathBuf {
PathBuf::from(env!("CARGO_MANIFEST_DIR")).join("tests/data/sample.mp4")
}

fn bundled_library_path() -> Option<PathBuf> {
let dir = option_env!("RS_MEDIAINFO_BUNDLED_DIR")?;
let dir = PathBuf::from(dir);
for name in ["MediaInfo.dll", "libmediainfo.so.0", "libmediainfo.0.dylib"] {
let candidate = dir.join(name);
if candidate.is_file() {
return Some(candidate);
}
}
None
}

/// Exercises the free function path. After the default-context
/// routing landed this is effectively a reuse benchmark, since the
/// first iteration initializes the cached context and every later
/// iteration goes through it.
fn bench_free_function(c: &mut Criterion) {
let path = sample_path();
c.bench_function("free_fn/parse_media_info_path", |b| {
b.iter(|| MediaInfo::parse_media_info_path(&path).expect("free function parse failed"));
});
}

/// Exercises the context path directly.
fn bench_context(c: &mut Criterion) {
let ctx = MediaInfoContext::new().expect("failed to construct context");
let path = sample_path();
c.bench_function("context/parse_media_info_path", |b| {
b.iter(|| {
ctx.parse_media_info_path(&path)
.expect("context parse failed")
});
});
}

/// Forces a fresh library load on every iteration by pinning
/// `library_file`, which bypasses the cached default context. This
/// reproduces the v0.1.0 "real dlopen per call" behavior so the
/// speedup from the reuse path is measurable.
fn bench_free_function_uncached(c: &mut Criterion) {
let Some(library_file) = bundled_library_path() else {
return;
};
let path = sample_path();
let options = ParseOptions::new().library_file(library_file);

c.bench_function("free_fn/parse_media_info_path_uncached", |b| {
b.iter(|| {
MediaInfo::parse_media_info_path_with_options(&path, &options)
.expect("uncached free function parse failed")
});
});
}

criterion_group!(
benches,
bench_free_function,
bench_context,
bench_free_function_uncached
);
criterion_main!(benches);
44 changes: 44 additions & 0 deletions examples/batch_parse.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
//! Example that builds a [`MediaInfoContext`] once and parses every
//! media file listed on the command line through the same context.
//!
//! This is the recommended shape for batch workloads — asset scanners,
//! import pipelines, directory walks — because the MediaInfo shared
//! library is loaded exactly once instead of on every call.
//!
//! Run with:
//!
//! ```text
//! cargo run --example batch_parse -- tests/data/sample.mp4 tests/data/sample.mkv
//! ```

use rsmediainfo::MediaInfoContext;
use std::env;

fn main() -> Result<(), Box<dyn std::error::Error>> {
let paths: Vec<String> = env::args().skip(1).collect();
if paths.is_empty() {
eprintln!("usage: batch_parse <media_path> [<media_path> ...]");
std::process::exit(2);
}

// One library load for the whole run — every subsequent parse
// reuses the same loaded copy.
let ctx = MediaInfoContext::new()?;
println!("loaded library version {}", ctx.library_version_string());

for path in &paths {
match ctx.parse_media_info_path(path) {
Ok(info) => {
println!("{}: {} tracks", path, info.tracks().len());
for track in info.tracks() {
println!(" - {}", track.track_type());
}
}
Err(err) => {
eprintln!("{}: {}", path, err);
}
}
}

Ok(())
}
45 changes: 45 additions & 0 deletions src/error.rs
Original file line number Diff line number Diff line change
Expand Up @@ -124,6 +124,26 @@ pub enum MediaInfoError {
/// in the buffer-protocol path. Wraps the underlying [`std::io::Error`].
#[error("I/O error: {0}")]
IoError(#[from] std::io::Error),

/// A parse call on a [`MediaInfoContext`](crate::MediaInfoContext)
/// was given [`ParseOptions`](crate::ParseOptions) whose
/// `library_file` or `library_search_dir` would require a different
/// shared library than the one the context already loaded.
///
/// A context is bound to a single library for its lifetime — if the
/// caller needs a different library, they should build a separate
/// context via
/// [`MediaInfoContext::with_library_file`](crate::MediaInfoContext::with_library_file)
/// or
/// [`MediaInfoContext::with_library_search_dir`](crate::MediaInfoContext::with_library_search_dir).
#[error("requested library '{requested}' does not match context library '{context}'")]
LibraryMismatch {
/// The library path the context was constructed with, or an
/// empty path when the context used the default search order.
context: PathBuf,
/// The conflicting library path the parse call requested.
requested: PathBuf,
},
}

impl MediaInfoError {
Expand Down Expand Up @@ -182,6 +202,31 @@ impl MediaInfoError {
pub fn xml_parse_error<S: Into<String>>(message: S) -> Self {
MediaInfoError::XmlParseError(message.into())
}

/// Builds a [`MediaInfoError::LibraryMismatch`] from the context
/// library path and the conflicting requested library path.
///
/// # Example
///
/// ```
/// use rsmediainfo::MediaInfoError;
///
/// let err = MediaInfoError::library_mismatch(
/// "/usr/lib/libmediainfo.so",
/// "/opt/custom/libmediainfo.so",
/// );
/// assert!(err.to_string().contains("/opt/custom/libmediainfo.so"));
/// ```
pub fn library_mismatch<C, R>(context: C, requested: R) -> Self
where
C: Into<PathBuf>,
R: Into<PathBuf>,
{
MediaInfoError::LibraryMismatch {
context: context.into(),
requested: requested.into(),
}
}
}

/// Routes XML parser errors into [`MediaInfoError::XmlParseError`] so the
Expand Down
7 changes: 6 additions & 1 deletion src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,8 @@
//! - structured track models with typed and dynamic attribute access
//! - raw text, JSON, XML, and `%`-delimited template output formats
//! - configurable parsing through [`ParseOptions`]
//! - reusable [`MediaInfoContext`] for batch workloads that parse many
//! files in a row without repeating the shared library load cost
//! - a typed error model ([`MediaInfoError`]) that surfaces every failure
//! mode of the underlying library through [`Result`]
//! - cross-platform support (Windows, macOS, Linux) for both x86_64 and
Expand Down Expand Up @@ -59,6 +61,8 @@
//! value
//! - `parse_xml` — construct a [`MediaInfo`] directly from a pre-generated
//! XML string with no shared library required at runtime
//! - `batch_parse` — build a [`MediaInfoContext`] once and parse a list of
//! files through it without reloading the shared library
//!
//! Run any of them with `cargo run --example <name>`.
//!
Expand All @@ -79,7 +83,8 @@ mod xml;

pub use error::{EncodingErrorMode, MediaInfoError, Result};
pub use mediainfo::{
MediaInfo, MediaInfoInput, MediaInfoSource, ParseOptions, ParseOutput, ReadSeek,
MediaInfo, MediaInfoContext, MediaInfoInput, MediaInfoSource, ParseOptions, ParseOutput,
ReadSeek,
};
pub use track::{AttributeValue, Track, TrackId};

Expand Down
Loading
Loading