Media Pipeline Plugin for NativePHP Mobile#
On-device photo and video compression for NativePHP Mobile. Pass a local path; get a smaller file back.
Overview#
nativephp/mobile-camera opens the camera or gallery and returns the original file. This plugin is the next step: hand that path to MediaPipeline::compressImage() or compressVideo(), and native code resizes and re-encodes on-device.
Why you’d buy it: camera plugins ship originals — multi‑MB photos and huge 4K clips. APIs, mobile data, and upload limits punish that. Media Pipeline keeps compression on the device (UIImage / AVAssetExportSession on iOS, BitmapFactory / LightCompressor on Android) so you don’t write Swift or Kotlin and you don’t send originals to a transcode API. You get a job id immediately; results arrive as Laravel events.
It does not pick or capture media (use mobile-camera). It does not upload (use Laravel Http). Scoped to compression so this package declares no camera, microphone, or photo-library permissions.
Installation#
composer require partek/media-pipeline
Don't forget to register the plugin:
php artisan native:plugin:register partek/media-pipeline
native:plugin:register adds the service provider to NativeServiceProvider::plugins() so native:run compiles the Swift/Kotlin bridges and merges nativephp.json. A package that is only composer require’d and never registered does nothing. Rebuild after register: php artisan native:run android or ios.
Usage#
Compression is async. Facade / JS calls return a job id (or a bridge ack containing one) immediately. Progress, success, and failure arrive later as events.
PHP (Livewire/Blade)#
Call compressImage() / compressVideo() from an event listener once you already have a file path — typically PhotoTaken, VideoRecorded, or MediaSelected from mobile-camera.
use PARTek\MediaPipeline\Facades\MediaPipeline;use PARTek\MediaPipeline\Events\CompressionProgress;use PARTek\MediaPipeline\Events\CompressionCompleted;use PARTek\MediaPipeline\Events\CompressionFailed;use Native\Mobile\Facades\Camera;use Native\Mobile\Attributes\On;use Native\Mobile\Events\Camera\PhotoTaken;use Illuminate\Support\Facades\Http; #[On(PhotoTaken::class)]public function onPhotoTaken(string $path): void{ $this->jobId = MediaPipeline::compressImage( $path, maxWidth: 1920, maxHeight: 1920, quality: 80, );} #[On(CompressionProgress::class)]public function onProgress(string $jobId, int $progress): void{ if ($jobId === $this->jobId) { $this->progress = $progress; }} #[On(CompressionCompleted::class)]public function onCompressed(string $jobId, string $path, int $sizeBytes): void{ if ($jobId !== $this->jobId) { return; } Http::attach('photo', file_get_contents($path), basename($path))->post('/upload');} #[On(CompressionFailed::class)]public function onFailed(string $jobId, string $message): void{ if ($jobId === $this->jobId) { $this->error = $message; }}
Use Native\Mobile\Attributes\On in SuperNative apps. OnNative extends Livewire and fatals in a Livewire-free app when the attribute is resolved.
use PARTek\MediaPipeline\Facades\MediaPipeline; $jobId = MediaPipeline::compressVideo($path, preset: 'medium', maxWidth: 1280);MediaPipeline::cancel($jobId);
JavaScript (Vue/React/Inertia)#
This package does not publish a #nativephp import (that alias is first-party) and is not an npm package name. Import the shipped module from Composer’s vendor path — same pattern as other third-party NativePHP plugins. The path below is relative to resources/js/; add a ../ if your file is nested further.
import { CompressImage, CompressVideo, Cancel } from '../../vendor/partek/media-pipeline/resources/js/index.js'; const image = await CompressImage({ path: localPath, max_width: 1920, max_height: 1920, quality: 80,}); const video = await CompressVideo({ path: localPath, preset: 'medium', max_width: 1280,}); await Cancel({ job_id: video.data?.job_id ?? video.job_id });// or: await Cancel(jobId);
Named camelCase aliases (compressImage, compressVideo, cancel) are also exported. Bridge names: MediaPipeline.CompressImage, MediaPipeline.CompressVideo, MediaPipeline.Cancel.
The JS helpers POST to /_native/api/call and return that JSON ack (accepted + job_id). They do not generate a job id unless you pass job_id; otherwise the native side assigns one. Progress / completion / failure are the PHP events below — listen with #[On(...)], or with NativePHP’s On() using the event class name if your SPA stack already uses #nativephp for events.
Events#
Event payload keys are camelCase (jobId, sizeBytes, durationSeconds). NativeComponent binds JSON keys to listener parameter names by literal match — snake_case keys silently mis-bind.
CompressionProgress#
Fired repeatedly during video compression (images finish too quickly).
Payload:
| Field | Type | Description |
|---|---|---|
jobId |
string | Job id returned by compressVideo() |
progress |
int | 0–100 |
use Native\Mobile\Attributes\On;use PARTek\MediaPipeline\Events\CompressionProgress; #[On(CompressionProgress::class)]public function onProgress(string $jobId, int $progress): void{ //}
CompressionCompleted#
Fired when image or video compression succeeds.
Payload:
| Field | Type | Description |
|---|---|---|
jobId |
string | Job id from the starting call |
path |
string | Local path to the compressed file |
sizeBytes |
int | Compressed file size |
width |
?int | Output width in pixels (when known) |
height |
?int | Output height in pixels (when known) |
durationSeconds |
?float | Video duration (video jobs only) |
CompressionFailed#
Fired when compression fails, or when a video job is cancelled (message is Cancelled.).
Payload:
| Field | Type | Description |
|---|---|---|
jobId |
string | Job id from the starting call |
message |
string | Failure reason |
Methods#
compressImage(string $path, ?int $maxWidth = 1920, ?int $maxHeight = 1920, int $quality = 80): string#
Resize and JPEG-compress an image already on-device (for example a path from Camera::getPhoto() or Camera::pickImages()). Returns $jobId. Fires CompressionCompleted or CompressionFailed.
| Parameter | Type | Default | Description |
|---|---|---|---|
$path |
string | required | Local file path |
$maxWidth |
?int | 1920 |
Longest-edge cap in pixels. null = no resize (see Platform notes) |
$maxHeight |
?int | 1920 |
Other-edge cap in pixels. null = no resize |
$quality |
int | 80 |
JPEG quality, 0–100 |
JS: CompressImage({ path, max_width?, max_height?, quality?, job_id? }).
compressVideo(string $path, string $preset = 'medium', ?int $maxWidth = 1280, ?int $targetBitrateKbps = null): string#
Transcode and compress a video already on-device (for example a path from Camera::recordVideo()). Returns $jobId. Fires CompressionProgress (0–100), then CompressionCompleted or CompressionFailed.
| Parameter | Type | Default | Description |
|---|---|---|---|
$path |
string | required | Local file path |
$preset |
string | 'medium' |
'low' | 'medium' | 'high' |
$maxWidth |
?int | 1280 |
Dimension cap in pixels. Applied on Android; accepted but unused on iOS |
$targetBitrateKbps |
?int | null |
Target bitrate in kbps. Applied on Android (converted to Mbps); accepted but unused on iOS |
JS: CompressVideo({ path, preset?, max_width?, target_bitrate_kbps?, job_id? }).
cancel(string $jobId): bool#
Cancel an in-progress video compression. No-op for image jobs (they finish too quickly). Returns whether a running job was found. A cancelled video job dispatches CompressionFailed with message Cancelled..
| Parameter | Type | Description |
|---|---|---|
$jobId |
string | Job id returned by compressVideo() |
JS: Cancel({ job_id }) or Cancel(jobId).
Platform notes#
Requires nativephp/mobile ^3.0 or ^4.0. No extra permissions on either platform — compression only reads and writes files your app already has a path to.
Android#
- Minimum SDK 26 (Android 8.0).
- Images:
BitmapFactorydecode, then JPEG re-encode.BitmapFactoryignores EXIF orientation and the output JPEG drops that tag, so the plugin readsExifInterface, swaps the size limits for quarter-turned sources, scales first, then bakes rotation into the smaller bitmap. Portrait camera photos save upright. - Resize is aspect-preserving and never upscales when both
max_widthandmax_heightare set. If both arenull, the image is re-encoded at original dimensions. - Video: LightCompressor 1.3.3 via JitPack.
low/medium/highmap toVideoQuality.max_widthmaps toVideoResizer.limitSize(caps either dimension, aspect preserved).target_bitrate_kbpsis converted to Mbps (kbps / 1000, minimum 1). Audio is kept; output is a streamable MP4. - Compressed files are written to the app cache directory.
- Cancel calls
VideoCompressor.cancel()and dispatchesCompressionFailed("Cancelled."). LightCompressor’s compressor is a singleton — cancelling while two video jobs are in flight can cancel the wrong one.
iOS#
- Minimum iOS 18.0. No extra
Info.plistkeys. - Images:
UIImage+UIGraphicsImageRenderer, thenjpegData. Never upscales. Resize runs only when bothmax_widthandmax_heightare provided; otherwise original dimensions are kept. - Video:
AVAssetExportSessionto MP4 withshouldOptimizeForNetworkUse.low/medium/highmap toAVAssetExportPresetLowQuality/MediumQuality/HighestQuality. There is no push progress API — progress is polled every 250ms.max_widthandtarget_bitrate_kbpsare accepted by the bridge but not applied; quality is the export preset only. - Compressed files are written to
FileManager.temporaryDirectory. - Cancel calls
AVAssetExportSession.cancelExport()and dispatchesCompressionFailed("Cancelled."). - NativePHP’s iOS Simulator PHP runtime (
libphp.a) is arm64-only. On an Intel Mac, use a physical device (or Apple Silicon). - If your code writes the source file (not a path from
mobile-camera), create the parent directory first (File::ensureDirectoryExists()/mkdir(..., true)). Empty source directories that only contain a.gitignoremay not exist after iOS bundle extraction; the write then fails with “failed to open stream” — that is not a compression bug.
Testing#
composer installvendor/bin/phpunit
The suite covers PHP-side behavior that does not need a device: UUID job ids, facade defaults (maxWidth/maxHeight 1920, quality 80, video preset medium / maxWidth 1280), cancel() returning false outside a built app, and a nativephp_call() parameter/response round trip.
Bridge functions and event dispatch only run inside a built NativePHP app. Test those on physical Android and iOS devices.
License#
Proprietary commercial software. See LICENSE.
Purchase grants use in an unlimited number of the licensee’s own applications, and modification for those applications. Redistribution, resale, sublicensing, or sharing repository access outside the licensee’s organization is not permitted. Provided “as is”, without warranty.