defmodule MobVideo do @moduledoc """ On-device video processing — a Mob plugin backed entirely by the platform video toolkits (Android `MediaExtractor`/`MediaMuxer`/`MediaMetadataRetriever`, iOS `AVFoundation`). No ffmpeg: the operations here are the ones the OS does natively, losslessly, and hardware-accelerated. Every call is **asynchronous** — the work runs on a background thread and the result arrives in the screen's `handle_info/2`. Pass a writable destination path under the app's own storage (e.g. `Mob.Storage.dir(:cache)`); `src` is a local file path (copy a picked/downloaded file into app storage first). ## Operations * `probe/2` — read a clip's metadata (no output file). * `clip/4` — cut a time range, **stream-copy** (no re-encode, lossless, fast). * `thumbnail/4` — extract a single frame as a JPEG. * `extract_audio/3` — pull the audio track into its own file. ## Result messages handle_info({:video, :info, %{duration_ms:, width:, height:, rotation:, has_audio:, bitrate:, frame_rate:}}, socket) handle_info({:video, :clipped, %{path:, duration_ms:}}, socket) handle_info({:video, :thumbnail, %{path:, width:, height:}}, socket) handle_info({:video, :audio_extracted, %{path:}}, socket) handle_info({:video, :error, reason}, socket) Common `reason` atoms: `:not_found` (src missing/unreadable), `:unsupported` (no video/audio track, or a container the OS can't demux), `:io_error` (the write failed), `:bad_range` (clip start/end outside the clip). ## Permissions Processing a file the app can already read needs **no** runtime permission. Reading from the shared media store (a path under the system gallery) needs `READ_MEDIA_VIDEO` (Android 13+) / Photos access (iOS) — request `:photos` via `Mob.Permissions` and pick through `MobPhotos` first, then hand the resulting local path here. ## Not (yet) here Re-encoding/transcoding, filters, overlays and concat-across-codecs are deliberately out of scope: they need a software codec pipeline (ffmpeg) with real size/licensing/perf cost. Everything above is stream-copy or single-frame decode, which the platform does for free. See the README for the rationale. """ @doc """ Read `src`'s metadata. Delivers `{:video, :info, map}` or `{:video, :error, reason}`. The map carries `:duration_ms`, `:width`, `:height` (pixels, pre-rotation), `:rotation` (degrees, 0/90/180/270), `:has_audio` (boolean), `:bitrate` (bits/sec, 0 if unknown) and `:frame_rate` (fps, 0.0 if unknown). """ @spec probe(Mob.Socket.t(), String.t()) :: Mob.Socket.t() def probe(socket, src) when is_binary(src) do :mob_video_nif.video_probe(src) socket end @doc """ Cut `src` to the `[start_ms, end_ms)` range, writing to `dst` by **stream copy** (no re-encode — lossless and near-instant). Delivers `{:video, :clipped, %{path:, duration_ms:}}` or `{:video, :error, reason}`. Options: * `:start_ms` — range start (default `0`). * `:end_ms` — range end (default the clip's duration). """ @spec clip(Mob.Socket.t(), String.t(), String.t(), keyword()) :: Mob.Socket.t() def clip(socket, src, dst, opts \\ []) when is_binary(src) and is_binary(dst) do start_ms = Keyword.get(opts, :start_ms, 0) end_ms = Keyword.get(opts, :end_ms, 0) :mob_video_nif.video_clip(src, dst, start_ms, end_ms) socket end @doc """ Extract a single frame from `src` at `:at_ms` (default `0`) as a JPEG at `dst`. Delivers `{:video, :thumbnail, %{path:, width:, height:}}` or `{:video, :error, reason}`. Options: * `:at_ms` — frame timestamp (default `0`). * `:max_width` — scale so the longer side is at most this many pixels (default `0` = full resolution). """ @spec thumbnail(Mob.Socket.t(), String.t(), String.t(), keyword()) :: Mob.Socket.t() def thumbnail(socket, src, dst, opts \\ []) when is_binary(src) and is_binary(dst) do at_ms = Keyword.get(opts, :at_ms, 0) max_width = Keyword.get(opts, :max_width, 0) :mob_video_nif.video_thumbnail(src, dst, at_ms, max_width) socket end @doc """ Pull `src`'s audio track into `dst` by stream copy. Delivers `{:video, :audio_extracted, %{path:}}` or `{:video, :error, reason}` (`:unsupported` when the clip has no audio track). """ @spec extract_audio(Mob.Socket.t(), String.t(), String.t()) :: Mob.Socket.t() def extract_audio(socket, src, dst) when is_binary(src) and is_binary(dst) do :mob_video_nif.video_extract_audio(src, dst) socket end end