vtx-sdk 0.1.14

Official SDK for developing VTX plugins using Rust and WebAssembly.
Documentation
//! Host-side FFmpeg helpers.

use crate::bindings::vtx::api::vtx_ffmpeg::{self, FfmpegOption, TranscodeProfile};
use crate::bindings::vtx::api::vtx_types::HttpResponse;
use crate::bindings::vtx::api::vtx_vfs::Buffer;
use crate::error::{VtxError, VtxResult};

/// Pure builder for `TranscodeProfile`.
///
/// This builder does not call any host APIs and can be tested locally.
pub struct TranscodeProfileBuilder {
    profile: String,
    input_id: String,
    options: Vec<FfmpegOption>,
}

impl TranscodeProfileBuilder {
    /// Create a new profile builder.
    ///
    /// - `profile`: Target FFmpeg profile (e.g., "mini", "remux", "thumbnail").
    /// - `input_id`: Input resource ID (UUID) or "pipe:0".
    pub fn new(profile: impl Into<String>, input_id: impl Into<String>) -> Self {
        Self {
            profile: profile.into(),
            input_id: input_id.into(),
            options: Vec::new(),
        }
    }

    /// Create a builder that uses stdin as input (`input_id = "pipe:0"`).
    pub fn new_pipe(profile: impl Into<String>) -> Self {
        Self::new(profile, "pipe:0")
    }

    /// Add a key/value FFmpeg option (encoded as `-key=value`).
    pub fn option(mut self, key: impl Into<String>, value: impl Into<String>) -> Self {
        self.options.push(FfmpegOption {
            key: key.into(),
            value: Some(value.into()),
        });
        self
    }

    /// Add a flag-style FFmpeg option (encoded as `-key`).
    pub fn flag(mut self, key: impl Into<String>) -> Self {
        self.options.push(FfmpegOption {
            key: key.into(),
            value: None,
        });
        self
    }

    /// Add a batch of key/value options.
    pub fn options<I, K, V>(mut self, options: I) -> Self
    where
        I: IntoIterator<Item = (K, V)>,
        K: Into<String>,
        V: Into<String>,
    {
        for (key, value) in options {
            self.options.push(FfmpegOption {
                key: key.into(),
                value: Some(value.into()),
            });
        }
        self
    }

    /// Helper: Set output format (equivalent to `-f=format`).
    pub fn format(self, format: &str) -> Self {
        self.option("f", format)
    }

    /// Helper: Set seek window (equivalent to `-ss` + optional `-t`).
    pub fn seek(self, start: &str, duration: Option<&str>) -> Self {
        let mut s = self.option("ss", start);
        if let Some(d) = duration {
            s = s.option("t", d);
        }
        s
    }

    /// Builds the `TranscodeProfile` payload.
    pub fn build(self) -> TranscodeProfile {
        TranscodeProfile {
            profile: self.profile,
            input_id: self.input_id,
            options: self.options,
        }
    }
}

/// IO boundary for executing a `TranscodeProfile`.
pub struct FfmpegExecutor;

impl FfmpegExecutor {
    /// Execute and return the stdout pipe buffer.
    pub fn execute_buffer(profile: &TranscodeProfile) -> VtxResult<Buffer> {
        vtx_ffmpeg::execute(profile).map_err(VtxError::from_host_message)
    }

    /// Execute and return an HTTP response (`200` with stdout pipe body).
    pub fn execute(profile: &TranscodeProfile) -> VtxResult<HttpResponse> {
        let buffer = Self::execute_buffer(profile)?;
        Ok(HttpResponse {
            status: 200,
            body: Some(buffer),
        })
    }
}

/// FFmpeg task builder for running host-side transcoding.
///
/// # Example
///
/// ```rust
/// use vtx_sdk::prelude::*;
///
/// fn handle_video(vid: String) -> VtxResult<Response> {
///     FfmpegTask::new("mini", vid)
///         .option("ss", "10")
///         .option("t", "30")
///         .execute()
/// }
/// ```
pub struct FfmpegTask {
    builder: TranscodeProfileBuilder,
}

impl FfmpegTask {
    /// Create a new FFmpeg task.
    ///
    /// - `profile`: Target FFmpeg profile (e.g., "mini", "remux", "thumbnail").
    /// - `input_id`: Input resource ID (UUID) or "pipe:0".
    pub fn new(profile: impl Into<String>, input_id: impl Into<String>) -> Self {
        Self {
            builder: TranscodeProfileBuilder::new(profile, input_id),
        }
    }

    /// Create a task that uses stdin as input (`input_id = "pipe:0"`).
    pub fn new_pipe(profile: impl Into<String>) -> Self {
        Self {
            builder: TranscodeProfileBuilder::new_pipe(profile),
        }
    }

    /// Add a key/value FFmpeg option (encoded as `-key=value`).
    pub fn option(mut self, key: impl Into<String>, value: impl Into<String>) -> Self {
        self.builder = self.builder.option(key, value);
        self
    }

    /// Add a flag-style FFmpeg option (encoded as `-key`).
    pub fn flag(mut self, key: impl Into<String>) -> Self {
        self.builder = self.builder.flag(key);
        self
    }

    /// Add a batch of key/value options.
    pub fn options<I, K, V>(mut self, options: I) -> Self
    where
        I: IntoIterator<Item = (K, V)>,
        K: Into<String>,
        V: Into<String>,
    {
        self.builder = self.builder.options(options);
        self
    }

    /// Helper: Set output format (equivalent to `-f=format`).
    pub fn format(self, format: &str) -> Self {
        self.option("f", format)
    }

    /// Helper: Set seek window (equivalent to `-ss` + optional `-t`).
    pub fn seek(self, start: &str, duration: Option<&str>) -> Self {
        let mut s = self.option("ss", start);
        if let Some(d) = duration {
            s = s.option("t", d);
        }
        s
    }

    /// Builds the `TranscodeProfile` payload.
    pub fn build(self) -> TranscodeProfile {
        self.builder.build()
    }

    /// Execute and return the stdout pipe buffer.
    ///
    /// This method performs an IO operation.
    pub fn execute_buffer(self) -> VtxResult<Buffer> {
        let profile = self.build();
        FfmpegExecutor::execute_buffer(&profile)
    }

    /// Execute and return an HTTP response (`200` with stdout pipe body).
    ///
    /// This method performs an IO operation.
    pub fn execute(self) -> VtxResult<HttpResponse> {
        let profile = self.build();
        FfmpegExecutor::execute(&profile)
    }
}