Media Pipeline Plugin for NativePHP Mobile#
On-device photo and video compression for NativePHP Mobile. Pass a local path; get a smaller file back.
Used in: PARqore’s own apps New Life Daily, FarmSeconds, and Hard Moments.
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.
Pairs with partek/frame-extractor: compress a clip here, then pull thumbnails or still frames from it there. Each installs on its own; use one or both.
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#
Test helpers#
On a NativePHP core with the macroable test harness (nativephp/mobile 4.0.0 or later), this plugin adds compression helpers to Native::fakeBridge() and Native::test(), so your app's tests can drive a screen that compresses media without a device:
use App\NativeComponents\UploadVideo;use Native\Mobile\Testing\Native; Native::fakeBridge()->withMediaPipeline(); Native::test(UploadVideo::class) ->tap('Compress') ->assertVideoCompressionRequested('/storage/clip.mov', 'medium') ->progressCompression(40) ->assertSee('40%') ->completeCompression('/tmp/clip.mp4', 2_400_000, 1280, 720, 12.5) ->assertSee('Ready to upload');
| Helper | Registered on | What it does |
|---|---|---|
withMediaPipeline() |
FakeBridge |
Answers the bridge as the native functions do: CompressImage and CompressVideo acknowledge with {accepted: true, job_id}, and Cancel answers {success}, which is true only for a compressVideo() job that is still running. |
assertImageCompressionRequested(?string $path = null, ?int $maxWidth = null) |
FakeBridge |
Asserts compressImage() reached the bridge: any image, or exactly $path and/or $maxWidth. |
assertVideoCompressionRequested(?string $path = null, ?string $preset = null) |
FakeBridge |
Asserts compressVideo() reached the bridge: any video, or exactly $path and/or $preset. |
assertCompressionCancelled(?string $jobId = null) |
FakeBridge |
Asserts cancel() reached the bridge, for any job or for $jobId. |
completeCompression(string $path = '/tmp/compressed.jpg', int $sizeBytes = 1024, ?int $width = null, ?int $height = null, ?float $durationSeconds = null, ?string $jobId = null) |
TestableComponent |
Delivers CompressionCompleted to the screen's #[On] handler. |
progressCompression(int $progress, ?string $jobId = null) |
TestableComponent |
Delivers CompressionProgress (0–100). |
failCompression(string $message = 'Compression failed', ?string $jobId = null) |
TestableComponent |
Delivers CompressionFailed. |
FakeBridge helpers also chain off the harness: Native::test(...)->assertCompressionCancelled() returns the harness.
- Job ids.
completeCompression()andfailCompression()use thejob_idof the lastcompressImage()orcompressVideo()call unless you pass$jobId.progressCompression()uses the lastcompressVideo()call's, because the device reports progress for video jobs only. Each fails with a message naming the helper when there is no such call. - Cancelling. On the device
cancel()only finds a video job that is still running.withMediaPipeline()does the same:cancel()returnsfalsefor image jobs, unknown ids, and video jobs you already finished withcompleteCompression()orfailCompression(). A cancelled video job then arrives asCompressionFailedwith the messageCancelled., so follow the cancel with->failCompression('Cancelled.'). - Optional payload fields.
completeCompression()sendswidth,heightanddurationSecondsonly when you pass them, so your handler's own defaults apply. The device always sendswidthandheight, plusdurationSecondsfor video jobs, so pass them if your handler's parameters have no default. - Failure messages. The default
Compression failedis a placeholder. Both platforms sendCancelled.for a cancelled video job. Other failures carry the platform's own message, for exampleCould not read source image at <path>.on iOS orImage compression failed.on Android. - Script the bridge first.
compressImage()andcompressVideo()return a job id either way, but withoutwithMediaPipeline()the fake bridge answersnullandcancel()returnsfalse. - Mocked facade. If you mock the facade (
MediaPipeline::shouldReceive(...)), calls never reach the bridge, so theFakeBridgehelpers see nothing and the event helpers can't find a job id. The event helpers still work when you pass$jobId. - The helpers are registered only while the app runs unit tests (
APP_ENV=testing). Production boots and v3 cores are untouched.
This package's suite#
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. MediaPipelineMacrosTest boots the NativePHP core with Testbench and drives the test helpers through the real facade and a small fixture screen.
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.