Skip to main content

mobiler_core/
bunny.rs

1//! Bunny.net Stream URL helpers — pure string builders, no I/O.
2//!
3//! Bunny is **not** a special widget in Mobiler; it is just URLs fed into the two general video
4//! surfaces:
5//! - the **embed iframe URL** ([`embed_url`] / [`embed_url_signed`]) → a [`web_view`](crate::web_view)
6//!   widget, which renders Bunny's own player (adaptive HLS + captions + quality + thumbnails, free,
7//!   on every platform);
8//! - the **direct HLS URL** ([`hls_url`]) → a [`video_player`](crate::video_player) widget, the
9//!   controllable native player (AVPlayer / Media3 / `<video>`).
10//!
11//! # ⚠️ Secret-key safety
12//! [`embed_url_signed`] hashes your **Token Authentication Key**, which is a **secret**. Never call
13//! it from a shipped client — the key is extractable from the binary, and native apps have no
14//! referrer/domain to fall back on. Run it **server-side** (this crate compiles for your Rust
15//! backend too) and pass the finished, time-boxed URL to the app via your model. On the client,
16//! default to the **unsigned** builders ([`embed_url`] / [`hls_url`]) with Bunny's domain/referrer
17//! allowlist, or a backend-signed URL.
18//!
19//! Protecting the **direct** URL (the HLS playlist + its segments) needs Bunny's *Advanced Token
20//! Authentication* (HMAC-SHA256 directory tokens), which is server-side-only and zone-specific — do
21//! that in your backend (see <https://docs.bunny.net/docs/cdn-token-authentication-advanced>) and
22//! feed the signed URL to [`video_player`](crate::video_player). For protected playback the simplest
23//! path is the signed **embed** ([`embed_url_signed`]) rendered in a [`web_view`](crate::web_view).
24
25use sha2::{Digest, Sha256};
26
27const EMBED_BASE: &str = "https://iframe.mediadelivery.net/embed";
28
29/// The unsigned Bunny embed-player URL for `video_id` in `library_id`, e.g.
30/// `https://iframe.mediadelivery.net/embed/12345/abc-123`. Render it in a
31/// [`web_view`](crate::web_view). Use when the video is public or protected by a Bunny
32/// domain/referrer allowlist.
33#[must_use]
34pub fn embed_url(library_id: impl AsRef<str>, video_id: impl AsRef<str>) -> String {
35    format!("{EMBED_BASE}/{}/{}", library_id.as_ref(), video_id.as_ref())
36}
37
38/// The **token-secured** Bunny embed-player URL: appends `?token=<hex>&expires=<unix_seconds>` where
39/// `token = SHA256_HEX(token_security_key + video_id + expires)` (Bunny *Embed View Token
40/// Authentication*). `expires` is a UNIX timestamp **in seconds**.
41///
42/// # ⚠️ Server-side only
43/// `token_security_key` is a secret — see the [module docs](self). Call this in your backend and
44/// pass the result to the client; do not embed the key in a shipped app.
45#[must_use]
46pub fn embed_url_signed(
47    library_id: impl AsRef<str>,
48    video_id: impl AsRef<str>,
49    token_security_key: impl AsRef<str>,
50    expires_unix_seconds: i64,
51) -> String {
52    let video_id = video_id.as_ref();
53    let token = embed_token(token_security_key.as_ref(), video_id, expires_unix_seconds);
54    format!(
55        "{EMBED_BASE}/{}/{video_id}?token={token}&expires={expires_unix_seconds}",
56        library_id.as_ref()
57    )
58}
59
60/// The Bunny *Embed View Token* for a video: `SHA256_HEX(token_security_key + video_id + expires)`.
61/// Exposed for callers that build the URL themselves; prefer [`embed_url_signed`].
62#[must_use]
63pub fn embed_token(token_security_key: &str, video_id: &str, expires_unix_seconds: i64) -> String {
64    let mut hasher = Sha256::new();
65    hasher.update(token_security_key.as_bytes());
66    hasher.update(video_id.as_bytes());
67    hasher.update(expires_unix_seconds.to_string().as_bytes());
68    hex::encode(hasher.finalize())
69}
70
71/// The unsigned **direct** HLS manifest URL for `video_id`, served from your Stream pull-zone host
72/// (`pull_zone_host` = e.g. `vz-xxxx.b-cdn.net`, no scheme): `https://<host>/<video_id>/playlist.m3u8`.
73/// Feed it to a [`video_player`](crate::video_player) — the controllable native player. For protected
74/// direct playback, sign it with Bunny Advanced Token Authentication server-side (see the
75/// [module docs](self)).
76#[must_use]
77pub fn hls_url(pull_zone_host: impl AsRef<str>, video_id: impl AsRef<str>) -> String {
78    format!(
79        "https://{}/{}/playlist.m3u8",
80        pull_zone_host.as_ref().trim_end_matches('/'),
81        video_id.as_ref()
82    )
83}
84
85#[cfg(test)]
86mod tests {
87    use super::*;
88
89    #[test]
90    fn embed_token_matches_bunny_formula() {
91        // SHA256_HEX("mysecretkey" + "abc-123" + "1700000000") — verified with `sha256sum`.
92        assert_eq!(
93            embed_token("mysecretkey", "abc-123", 1_700_000_000),
94            "54ce9c375a5def073eb128910d013c1f5ae5385fae501af7e807fe4f8de927f4"
95        );
96    }
97
98    #[test]
99    fn url_builders_shape() {
100        assert_eq!(embed_url("12345", "abc-123"), "https://iframe.mediadelivery.net/embed/12345/abc-123");
101        assert_eq!(
102            embed_url_signed("12345", "abc-123", "mysecretkey", 1_700_000_000),
103            "https://iframe.mediadelivery.net/embed/12345/abc-123?token=54ce9c375a5def073eb128910d013c1f5ae5385fae501af7e807fe4f8de927f4&expires=1700000000"
104        );
105        assert_eq!(hls_url("vz-abc.b-cdn.net", "abc-123"), "https://vz-abc.b-cdn.net/abc-123/playlist.m3u8");
106        assert_eq!(hls_url("vz-abc.b-cdn.net/", "abc-123"), "https://vz-abc.b-cdn.net/abc-123/playlist.m3u8");
107    }
108}